Compare commits

...

286 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
Thomas Pelletier 2eff2d082a Rename branch v2-wip -> v2 2021-04-15 11:26:14 -04:00
Vincent Serpoul 59cddbc573 Golangci-lint v2 part two (#498) 2021-04-15 10:29:46 -04:00
Thomas Pelletier 9e122af5fc encoder: support multiline strings + local options 2021-04-10 17:58:37 -04:00
Cameron Moore ed1f9ed9de Add sanity check tests to benchmark dataset (#497)
Marshal results into JSON and ensure all runners match
2021-04-09 11:27:22 -04:00
Cameron Moore 466bfe8664 encoder: inline tables create map in nil interface (#496)
Co-authored-by: Thomas Pelletier <pelletier.thomas@gmail.com>
2021-04-09 09:02:00 -04:00
Thomas Pelletier e1f035461b encoder: simplify quoted strings escaping 2021-04-08 22:02:41 -04:00
Thomas Pelletier 84f9e9bceb ci: run benchmark tests 2021-04-08 19:43:14 -04:00
Thomas Pelletier ca41df4a59 encoder: only create empty map when target exists 2021-04-08 19:40:34 -04:00
Thomas Pelletier f2378983d9 encoder: added test for #287 2021-04-08 10:24:38 -04:00
Thomas Pelletier 37714006b6 V2 Marshaler MVP (#495) 2021-04-08 10:07:29 -04:00
Thomas Pelletier 275e366c17 decoder: handle casting local date into time.Time
Refs #494
2021-04-08 08:00:31 -04:00
Vincent Serpoul 18af62d3ea Golangci-lint v2 part one (#492) 2021-04-07 13:39:01 -04:00
jidicula af00765ca0 Address golangci-lint warnings in unmarshal_imported_test.go (#493)
* refactor(tracker): Remove unreachable return

* refactor(unmarshal_imported_test): Mark unused camelCase test

golangci-lint indicates `TestUnmarshalCamelCaseKey` as unused (it's
currently skipped).

* refactor(unmarshal_imported_test): Mark unused type

golangci-lint indicates `customPointerMarshaler` as unused.

* refactor(unmarshal_imported_test): Mark unused type

golangci-lint indicates `textPointerMarshaler` as unused.

* refactor(unmarshal_imported_test): Mark unused type

golangci-lint indicates `precedentMarshaler` and its methods as
unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `testDurationToml2` as unused.

* refactor(unmarshal_imported_test): Mark unused type

golangci-lint indicates `testBadDuration` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `testDurationToml` as unused.

* refactor(unmarshal_imported_test): Mark unused type

golangci-lint indicates `testDuration` as unused.

* refactor(unmarshal_imported_test): Mark unused type

golangci-lint indicates `testDocCustomTag` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `testDocCustomTagData` as unused.

* refactor(unmarshal_imported_test): Mark unused type

golangci-lint indicates `testDocBasicsCustomTag` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `testDocBasicToml` as unused.

* refactor(unmarshal_imported_test): Mark unused type

golangci-lint indicates `structArrayNoTag` as unused.

* refactor(unmarshal_imported_test): Mark unused type

golangci-lint incorrectly indicates `check` as unused.

* refactor(unmarshal_imported_test): Mark unused struct field

golangci-lint indicates `testDoc.err` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `customMultilineTagTestToml` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `customCommentedTagTestToml` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `customCommentTagTestToml` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `customTagTestToml` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `mapsTestToml` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `mapsTestData` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `commentTestToml` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `nestedCustomMarshalerToml` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `nestedCustomMarshalerData` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `customMarshalerToml` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `mapTestDoc` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `quotedKeyMarshalTestToml` as unused.

* refactor(unmarshal_imported_test): Mark unused var

golangci-lint indicates `quotedKeyMarshalTestData` as unused.
2021-04-05 14:30:17 -04:00
Cameron Moore 5f877c52fd Add additional dataset to benchmarks (#490)
Adding several files to stress test the unmarshaller.  Most were
converted from JSON so they may not be very realistic use cases.

```
goos: linux
goarch: amd64
pkg: github.com/pelletier/go-toml/v2/benchmark
cpu: Intel(R) Core(TM) i7-7600U CPU @ 2.80GHz
BenchmarkUnmarshalDataset/config/v2-2                 16          66339063 ns/op          15.81 MB/s    16850159 B/op     645454 allocs/op
BenchmarkUnmarshalDataset/config/v1-2                  7         147289186 ns/op           7.12 MB/s    60669811 B/op     870486 allocs/op
BenchmarkUnmarshalDataset/config/bs-2                 12          88966009 ns/op          11.79 MB/s    29949268 B/op     705573 allocs/op
BenchmarkUnmarshalDataset/canada/v2-2                  8         145433377 ns/op          15.14 MB/s    74282227 B/op     895641 allocs/op
BenchmarkUnmarshalDataset/canada/v1-2                  3         434913677 ns/op           5.06 MB/s    138664290 B/op   1897300 allocs/op
BenchmarkUnmarshalDataset/citm_catalog/v2-2                   19          55256979 ns/op          10.10 MB/s    37122952 B/op     369492 allocs/op
BenchmarkUnmarshalDataset/citm_catalog/v1-2                   10         110343191 ns/op           5.06 MB/s    54743595 B/op     727704 allocs/op
BenchmarkUnmarshalDataset/citm_catalog/bs-2                   21          51634081 ns/op          10.81 MB/s    17196872 B/op     325830 allocs/op
BenchmarkUnmarshalDataset/twitter/v2-2                        50          26660937 ns/op          16.57 MB/s    15580238 B/op     156394 allocs/op
BenchmarkUnmarshalDataset/twitter/v1-2                        30          43128488 ns/op          10.25 MB/s    21203420 B/op     266110 allocs/op
BenchmarkUnmarshalDataset/twitter/bs-2                        48          27337976 ns/op          16.16 MB/s     8795405 B/op     145370 allocs/op
BenchmarkUnmarshalDataset/code/v2-2                            4         276279202 ns/op           9.71 MB/s    59293948 B/op    2907227 allocs/op
BenchmarkUnmarshalDataset/code/v1-2                            3         421910642 ns/op           6.36 MB/s    161733770 B/op   2478194 allocs/op
BenchmarkUnmarshalDataset/code/bs-2                            4         323158157 ns/op           8.31 MB/s    133056988 B/op   1439475 allocs/op
BenchmarkUnmarshalDataset/example/v2-2                      2444            479553 ns/op          16.89 MB/s      237606 B/op       3609 allocs/op
BenchmarkUnmarshalDataset/example/v1-2                      1321            911995 ns/op           8.88 MB/s      377502 B/op       6133 allocs/op
BenchmarkUnmarshalDataset/example/bs-2                      1898            555649 ns/op          14.58 MB/s      178485 B/op       3362 allocs/op
BenchmarkUnmarshalSimple/v2-2                             896760              1200 ns/op
BenchmarkUnmarshalSimple/v1-2                             207364              6070 ns/op
BenchmarkUnmarshalSimple/bs-2                             420952              2925 ns/op
BenchmarkReferenceFile/v2-2                                29473             39433 ns/op         132.93 MB/s       11812 B/op        253 allocs/op
BenchmarkReferenceFile/v1-2                                 2823            361383 ns/op          14.51 MB/s      136470 B/op       2745 allocs/op
BenchmarkReferenceFile/bs-2                                 3097            391116 ns/op          13.40 MB/s       80795 B/op       1729 allocs/op
PASS
ok      github.com/pelletier/go-toml/v2/benchmark       34.255s
```
2021-04-01 10:13:13 -04:00
Thomas Pelletier 92b16cad91 Simplify context implementation and fix new lines bug 2021-03-31 09:57:19 -04:00
Thomas Pelletier 4a4c2c2a5f Update readme 2021-03-31 09:15:33 -04:00
Thomas Pelletier 5d905981cf CI: add dependabot 2021-03-30 22:03:39 -04:00
Thomas Pelletier 7ccacf158e Test for #252 2021-03-30 21:53:01 -04:00
Thomas Pelletier 739ceda96c Move github pull request template 2021-03-30 21:45:43 -04:00
Thomas Pelletier 32da85ab11 Decoding error position tracking 2021-03-30 21:43:57 -04:00
Thomas Pelletier 18d45c446b wip: decoder errors 2021-03-30 19:52:02 -04:00
Thomas Pelletier bcd5333b03 Enable ci on v2-wip branch pull requests 2021-03-30 12:38:26 -04:00
Cameron Moore e5255a5be2 Set bytes in ReferenceFile benchmark to show throughput results (#489) 2021-03-30 12:34:25 -04:00
Thomas Pelletier cf288a51c5 Wip errors reporting 2021-03-30 10:59:35 -04:00
Thomas Pelletier 72a1afdcb2 Add some unsafe helper to track errors 2021-03-29 22:33:28 -04:00
Thomas Pelletier 2714786b37 Add decoder interface 2021-03-29 21:30:41 -04:00
Thomas Pelletier 51d78a5f0c Fix unmarshaling of literal keys
Ref #427.
2021-03-29 20:58:51 -04:00
Thomas Pelletier 78389c641a Test for #475 2021-03-29 20:46:09 -04:00
Thomas Pelletier c3fc668f27 Test for #458 2021-03-29 20:38:48 -04:00
Thomas Pelletier 7f016efe03 Test for #484 2021-03-29 20:28:51 -04:00
Thomas Pelletier 269b742eb2 Enable race condition detector in CI 2021-03-29 20:17:05 -04:00
Cameron Moore 7d8ea80dc3 Fix scanning of float with leading zero (#486) 2021-03-29 20:07:26 -04:00
Cameron Moore 6165b9454f Identify test helper functions (#487) 2021-03-29 20:06:46 -04:00
Thomas Pelletier 2ddbf6be6d Implement duplicate and key types check 2021-03-29 10:45:50 -04:00
Thomas Pelletier da21b0aecf wip: correctness pass on the AST 2021-03-28 22:12:19 -04:00
Thomas Pelletier 829c005784 Fix unicode decoding 2021-03-28 11:03:43 -04:00
Thomas Pelletier b24eb93e8e Fix literal multiline parsing 2021-03-28 00:23:50 -04:00
Thomas Pelletier 7dc5550057 Fix multiline basic string parsing 2021-03-28 00:17:58 -04:00
Thomas Pelletier 9a436c7eeb Remove logging in test 2021-03-28 00:06:40 -04:00
Thomas Pelletier 72c999ecbf Fix trailing commas in arrays 2021-03-28 00:04:25 -04:00
Thomas Pelletier e5a091a092 Don't depend on my computer path 2021-03-27 23:43:24 -04:00
Thomas Pelletier 317b36b24b Add back license 2021-03-26 09:53:21 -04:00
Thomas Pelletier 636a75f316 Import tomltestgen
Handful are failing.
2021-03-26 09:51:35 -04:00
Thomas Pelletier 390927a0cd Reuse AST storage between top-level expressions
```
Comparing:
	old: v2-wip/1da2fc7 (2021-03-25 20:38:05 -0400 -0400)
	run: v2-wip/3f23ab9 (2021-03-25 22:35:06 -0400 -0400)
-----------------------------------------------------------
name                  old time/op    new time/op    delta
UnmarshalSimple/v2-8     700ns ± 3%     705ns ± 2%     ~     (p=0.690 n=5+5)
UnmarshalSimple/v1-8    3.85µs ± 1%    4.02µs ± 4%   +4.19%  (p=0.032 n=5+5)
UnmarshalSimple/bs-8    2.34µs ± 2%    2.38µs ± 3%     ~     (p=0.310 n=5+5)
ReferenceFile/v2-8      32.2µs ±13%    23.9µs ± 1%  -25.79%  (p=0.008 n=5+5)
ReferenceFile/v1-8       270µs ± 2%     264µs ± 2%     ~     (p=0.095 n=5+5)
ReferenceFile/bs-8       291µs ± 0%     294µs ± 0%   +0.88%  (p=0.008 n=5+5)

name                  old alloc/op   new alloc/op   delta
ReferenceFile/v2-8      37.1kB ± 0%     6.7kB ± 0%  -81.91%  (p=0.008 n=5+5)
ReferenceFile/v1-8       131kB ± 0%     131kB ± 0%     ~     (p=0.444 n=5+5)
ReferenceFile/bs-8      80.8kB ± 0%    80.8kB ± 0%     ~     (p=0.571 n=5+5)

name                  old allocs/op  new allocs/op  delta
ReferenceFile/v2-8         152 ± 0%       148 ± 0%   -2.63%  (p=0.008 n=5+5)
ReferenceFile/v1-8       2.65k ± 0%     2.65k ± 0%     ~     (all equal)
ReferenceFile/bs-8       1.73k ± 0%     1.73k ± 0%     ~     (all equal)

~/s/g/p/g/benchmark$ go test -bench=.
goos: linux
goarch: amd64
pkg: github.com/pelletier/go-toml/v2/benchmark
cpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz
BenchmarkUnmarshalSimple/v2-8         	 1692444	       710.7 ns/op
BenchmarkUnmarshalSimple/v1-8         	  307609	      3862 ns/op
BenchmarkUnmarshalSimple/bs-8         	  520429	      2285 ns/op
BenchmarkReferenceFile/v2-8           	   50395	     24006 ns/op	    6704 B/op	     148 allocs/op
BenchmarkReferenceFile/v1-8           	    4144	    264655 ns/op	  130567 B/op	    2649 allocs/op
BenchmarkReferenceFile/bs-8           	    3969	    293635 ns/op	   80784 B/op	    1729 allocs/op
PASS
ok  	github.com/pelletier/go-toml/v2/benchmark	8.143s
```
2021-03-25 22:37:16 -04:00
Thomas Pelletier 3f23ab97e0 Revert os/go version to fit in github pop-up 2021-03-25 21:16:31 -04:00
Thomas Pelletier 47611ff9ea Shorter names for CI 2021-03-25 21:14:59 -04:00
Thomas Pelletier f4ac7f7bfa Multi OS testing 2021-03-25 21:12:19 -04:00
Thomas Pelletier e75f23188d Add v2-wip to codeql branches 2021-03-25 21:08:03 -04:00
Thomas Pelletier 6c8adbcb17 Remove azure pipeline 2021-03-25 21:06:54 -04:00
Thomas Pelletier ffc7d3ba6e Port codeql 2021-03-25 21:06:34 -04:00
Thomas Pelletier 4efec6b76a Add github actions workflow 2021-03-25 21:05:07 -04:00
Thomas Pelletier 0fcf06e374 Update todo 2021-03-25 20:49:27 -04:00
Thomas Pelletier 1d332cd112 Add documentation for the AST 2021-03-25 20:46:31 -04:00
Thomas Pelletier 9d3a912da0 Remove unused interface
Comparing:
	old: v2-wip/17299c9 (2021-03-25 20:19:40 -0400 -0400)
	run: v2-wip/1da2fc7 (2021-03-25 20:38:05 -0400 -0400)
-----------------------------------------------------------
name                  old time/op    new time/op    delta
UnmarshalSimple/v2-8     755ns ± 3%     700ns ± 3%   -7.26%  (p=0.008 n=5+5)
UnmarshalSimple/v1-8    3.87µs ± 0%    3.85µs ± 1%     ~     (p=0.254 n=4+5)
UnmarshalSimple/bs-8    2.44µs ± 4%    2.34µs ± 2%     ~     (p=0.056 n=5+5)
ReferenceFile/v2-8      33.5µs ± 7%    32.2µs ±13%     ~     (p=0.421 n=5+5)
ReferenceFile/v1-8       269µs ± 3%     270µs ± 2%     ~     (p=1.000 n=5+5)
ReferenceFile/bs-8       296µs ± 2%     291µs ± 0%     ~     (p=0.095 n=5+5)

name                  old alloc/op   new alloc/op   delta
ReferenceFile/v2-8      38.9kB ± 0%    37.1kB ± 0%   -4.77%  (p=0.008 n=5+5)
ReferenceFile/v1-8       131kB ± 0%     131kB ± 0%     ~     (all equal)
ReferenceFile/bs-8      80.8kB ± 0%    80.8kB ± 0%     ~     (p=0.841 n=5+5)

name                  old allocs/op  new allocs/op  delta
ReferenceFile/v2-8         181 ± 0%       152 ± 0%  -16.02%  (p=0.008 n=5+5)
ReferenceFile/v1-8       2.65k ± 0%     2.65k ± 0%     ~     (all equal)
ReferenceFile/bs-8       1.73k ± 0%     1.73k ± 0%     ~     (all equal)
2021-03-25 20:38:45 -04:00
Thomas Pelletier 1da2fc7e28 Minimal shared cache for struct field paths 2021-03-25 20:19:58 -04:00
Thomas Pelletier 17299c937b Update readme 2021-03-25 19:56:40 -04:00
Thomas Pelletier 1bae751a45 Linear array storage for AST 2021-03-25 19:56:02 -04:00
Thomas Pelletier 8a8d1233bb First benchmark!
~/s/g/p/g/benchmark$ go test -bench=.
goos: linux
goarch: amd64
pkg: github.com/pelletier/go-toml/v2/benchmark
cpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz
BenchmarkUnmarshalSimple/v2-8         	 1607115	       742.0 ns/op
BenchmarkUnmarshalSimple/v1-8         	  307977	      3915 ns/op
BenchmarkUnmarshalSimple/bs-8         	  516754	      2330 ns/op
BenchmarkReferenceFile/v2-8           	    9604	    129158 ns/op	  111422 B/op	    1381 allocs/op
BenchmarkReferenceFile/v1-8           	    4521	    263808 ns/op	  130566 B/op	    2649 allocs/op
BenchmarkReferenceFile/bs-8           	    4070	    296271 ns/op	   80784 B/op	    1729 allocs/op
PASS
ok  	github.com/pelletier/go-toml/v2/benchmark	8.139s
2021-03-24 22:15:12 -04:00
Thomas Pelletier ad538d97c9 Delete reflectbuild 2021-03-24 21:06:38 -04:00
Thomas Pelletier 43fc2fa552 Factor pointer handling 2021-03-24 21:05:44 -04:00
Thomas Pelletier dd5837651d Support TextUnmarshaler 2021-03-24 21:02:02 -04:00
Thomas Pelletier a0d031abec Arrays support 2021-03-24 20:21:55 -04:00
Thomas Pelletier a25f636a07 Add array support todo 2021-03-23 21:18:19 -04:00
Thomas Pelletier a3b7e1e353 Fix table array into pointer to slice 2021-03-23 21:14:54 -04:00
Thomas Pelletier bfeb32c9ce Make unmarshal to interface{} consistent with encoding/json 2021-03-23 20:03:45 -04:00
Thomas Pelletier 0703eeb262 Fix bug parsing anonymous structs 2021-03-23 18:01:14 -04:00
Thomas Pelletier d458ddf4d4 Add TODO 2021-03-23 09:45:12 -04:00
Thomas Pelletier 4038ec3dae Overflow checks 2021-03-23 09:44:03 -04:00
Thomas Pelletier 5b92184e42 Cast map key type 2021-03-23 09:20:28 -04:00
Thomas Pelletier c6f117c45d Handle pointers in slices 2021-03-23 09:15:48 -04:00
Thomas Pelletier e78ccff9a4 Fix parsing integer 0 2021-03-23 09:02:48 -04:00
Thomas Pelletier b8da9d1854 Fix datetime error checking 2021-03-23 08:54:44 -04:00
Thomas Pelletier e5d63aa8fc Add some type conversions 2021-03-22 20:09:11 -04:00
Thomas Pelletier ac2d6e2030 Handle unmarshalling value to nil ptr. 2021-03-22 20:03:35 -04:00
Thomas Pelletier fcc91f2618 Progress on date/times 2021-03-22 09:59:15 -04:00
Thomas Pelletier 8b34e54764 Improve DOT representation for AST 2021-03-18 22:10:31 -04:00
Thomas Pelletier ebffe6db83 Naive implementation of anonymous structs 2021-03-18 21:11:51 -04:00
Thomas Pelletier 9ec4e86883 Handle struct field name variations 2021-03-18 20:42:41 -04:00
Thomas Pelletier 93a7b0d77d Skip AST branches that don't exist in the target 2021-03-18 20:30:51 -04:00
Thomas Pelletier 3e8b8db786 Unmarshal into pointers 2021-03-18 20:02:32 -04:00
Thomas Pelletier 8957a768ef Next stop: pointers 2021-03-18 19:52:11 -04:00
Thomas Pelletier fad86a5f24 Test for sub-table in array table into structs 2021-03-18 19:48:09 -04:00
Thomas Pelletier 548b128e67 Fix multiple sub-table in array table 2021-03-18 19:42:48 -04:00
Thomas Pelletier a577df2dbb wip 2021-03-18 17:19:50 -04:00
Thomas Pelletier cb678e6221 Passing unmarshal of array table into interfaces 2021-03-18 08:47:50 -04:00
Thomas Pelletier 939f889666 wip: figuring out unmarshaling to interfaces 2021-03-17 09:57:50 -04:00
Thomas Pelletier f9f9ccb777 Basic array table implementation 2021-03-16 10:24:19 -04:00
Thomas Pelletier c6892fcf5a wip array table 2021-03-15 19:35:48 -04:00
Thomas Pelletier 844c9093a2 Add todo 2021-03-15 09:50:01 -04:00
Thomas Pelletier 37d06dabcf Unmarshal into maps 2021-03-15 09:49:10 -04:00
Thomas Pelletier 1718142ede More todos in README 2021-03-15 09:04:54 -04:00
Thomas Pelletier ad64e5d2e2 Update README for v2 work 2021-03-15 08:53:16 -04:00
Thomas Pelletier 00b2f776a9 Replace branch with AST version 2021-03-15 08:46:35 -04:00
Thomas Pelletier b8df31de84 Comment out date/time tests for now 2021-03-14 18:13:57 -04:00
Thomas Pelletier 16a336b4f3 Remove todos that don't make sense anymore 2021-03-14 18:10:59 -04:00
Thomas Pelletier 590d674153 Unmarshal ints and floats 2021-03-14 18:06:34 -04:00
Thomas Pelletier 9a1cfcdd8e Replace parser's int or float code with scanner 2021-03-14 17:22:53 -04:00
Thomas Pelletier 590d7faf65 Parser emits AST node for all kinds of strings 2021-03-14 16:16:29 -04:00
Thomas Pelletier de035f0fed Standard tables in parser 2021-03-14 16:11:23 -04:00
Thomas Pelletier 04925e4882 Handle bools 2021-03-14 15:52:22 -04:00
Thomas Pelletier 3760527218 Unmarshal tests 2021-03-13 23:42:38 -05:00
Thomas Pelletier fa7ee6461a Inline tables 2021-03-13 23:06:16 -05:00
Thomas Pelletier fbf01f7683 Handle Table 2021-03-13 22:48:31 -05:00
Thomas Pelletier a0548e793c Unmarshal slices of strings 2021-03-13 22:07:36 -05:00
Thomas Pelletier 1fafb71fd9 LF 2021-03-13 18:51:45 -05:00
Thomas Pelletier d8be04d4a8 Handle simple string slice 2021-03-13 18:45:03 -05:00
Thomas Pelletier 21d3e85fcc Playing with an AST
Idea would be to build a light AST as a first pass, then have the
unmarshaler and Document parser do what they need with it.
2021-03-13 11:38:09 -05:00
Thomas Pelletier 93a74fca35 todo: inline tables 2021-03-08 21:59:43 -05:00
Thomas Pelletier a1c9b661b4 Allocate slice if needed 2021-03-08 21:41:03 -05:00
Thomas Pelletier 87b9d1cf98 Handle overflows 2021-03-08 21:01:53 -05:00
Thomas Pelletier 90f3b658c6 Support type aliases 2021-03-08 20:27:04 -05:00
Thomas Pelletier c35bcc5519 Convert returns pointer if a pointer is passed 2021-03-08 10:01:56 -05:00
Thomas Pelletier f698c102c7 Convert should only handle specific types 2021-03-08 09:42:19 -05:00
Thomas Pelletier 2cee819ce4 Going back and forth on whether types should always be converted 2021-03-02 10:37:17 -05:00
Thomas Pelletier bf051f1718 Fixed some tests 2021-03-01 20:50:18 -05:00
Thomas Pelletier c77f1d815c Skip default tags tests 2021-02-19 19:48:43 -05:00
Thomas Pelletier d24deebee3 wip making reflection tests pass 2021-02-19 19:26:46 -05:00
Thomas Pelletier 4526154571 wip 2021-02-19 09:39:50 -05:00
Thomas Pelletier 978143ce99 Ensure that slices have been allocated when entering array 2021-02-19 08:53:19 -05:00
Thomas Pelletier 7f9822db35 Target set methods now check for types 2021-02-19 08:39:18 -05:00
Thomas Pelletier 052233e858 wip: debugging maps 2021-02-18 23:30:46 -05:00
Thomas Pelletier 629a2475a9 Handle maps 2021-02-18 22:39:33 -05:00
Thomas Pelletier 46573551f1 WIP constructing pointers 2021-02-18 21:24:26 -05:00
Thomas Pelletier 1f41c556e8 Handle missing fields in structs 2021-02-13 14:35:39 -05:00
Thomas Pelletier 9ac08febd2 DateTime/LocalDate/LocalTime implementation 2021-02-10 20:58:22 -05:00
Thomas Pelletier 2341b4df00 Comment out json-based comparison 2021-02-10 18:42:43 -05:00
Thomas Pelletier 6e79ce63c2 Reflect write to embeded structs 2021-02-10 18:34:54 -05:00
Thomas Pelletier e2a07a3b92 Handle interface field dereference 2021-02-10 11:43:57 -05:00
Thomas Pelletier 0dad1a950c Import Unmarshal tests (not passing) 2021-02-10 10:41:34 -05:00
Thomas Pelletier 27f0aeee30 Import localtime 2021-02-10 10:41:23 -05:00
Thomas Pelletier 721fa81f2e Support numbers 2021-02-10 10:00:08 -05:00
Thomas Pelletier f6a13d6e05 wip numbers 2021-02-09 20:44:54 -05:00
Thomas Pelletier 2660bb8426 Test inline tables 2021-02-09 19:36:14 -05:00
Thomas Pelletier 84282bbfd3 Move parser code 2021-02-09 19:26:50 -05:00
Thomas Pelletier 0982fd5f1f Check unhandled error 2021-02-09 19:23:45 -05:00
Thomas Pelletier 7dbf7554c4 Nested arrays 2021-02-09 19:23:10 -05:00
Thomas Pelletier 2790964270 Bool value 2021-02-08 20:44:23 -05:00
Thomas Pelletier 3488a91eff Array values 2021-02-08 20:39:04 -05:00
Thomas Pelletier 0e8fd64203 Move tests out of the package 2021-02-08 09:18:42 -05:00
Thomas Pelletier 70d41bd750 Add more tests for unmarshal array tables 2021-02-08 09:16:26 -05:00
Thomas Pelletier a197513ce7 Simple table array 2021-02-08 09:08:42 -05:00
Thomas Pelletier bd8df24646 Parse tables 2021-02-07 18:30:33 -05:00
Thomas Pelletier 89052d60b4 Very beginning of unmarshaler + builder interface 2021-02-06 23:20:26 -05:00
Thomas Pelletier 9fa2fd413d Implement inline tables 2021-02-06 09:33:20 -05:00
Thomas Pelletier b1e11f82a9 Implement array values 2021-02-06 09:27:24 -05:00
Thomas Pelletier 165f65408d Implement tables 2021-02-06 09:09:41 -05:00
Thomas Pelletier 540c2a7b59 Fix parsing bugs + boolean impl 2021-02-06 08:54:40 -05:00
Thomas Pelletier a466f0ca79 Multiline literal strings 2021-02-06 08:01:38 -05:00
Thomas Pelletier 736a75748b Multiline basic string parsing 2021-02-05 17:46:40 -05:00
Thomas Pelletier ca12c0670d wip parsing 2021-02-05 14:48:16 -05:00
Thomas Pelletier 0ee0fe7f7c Trying the scanner approach 2021-02-04 10:14:11 -05:00
Thomas Pelletier b123c357c5 Add tokens to Document 2021-02-02 20:54:20 -05:00
Thomas Pelletier 94ad175728 wip 2021-02-02 10:55:23 -05:00
Thomas Pelletier 1e8b0dc3c9 Rename to lexer and split in files 2021-02-02 08:28:30 -05:00
Thomas Pelletier 7300b6a97b Array tables 2021-02-02 08:19:04 -05:00
Thomas Pelletier aae4656c64 Standard Table 2021-02-01 22:03:53 -05:00
Thomas Pelletier 44f7a7aead Inline tables 2021-02-01 21:41:34 -05:00
Thomas Pelletier bac65cc530 Array implementation 2021-02-01 21:25:20 -05:00
Thomas Pelletier 91d7afbc0a Parse rvalue string 2021-02-01 20:54:04 -05:00
Thomas Pelletier 7b4d82a939 Remove error handling for rune 2021-02-01 20:25:31 -05:00
Thomas Pelletier 2ab0f8c733 Default to use bytes instead of runes
benchmark               old ns/op     new ns/op     delta
BenchmarkParseAll-8     3238          1941          -40.06%
2021-02-01 20:20:24 -05:00
Thomas Pelletier b96c535061 Check for allocs 2021-02-01 19:25:07 -05:00
Thomas Pelletier fd961100c1 Boolean values 2021-02-01 19:19:40 -05:00
Thomas Pelletier 1c7e9fe3af Dotted keys 2021-02-01 19:07:51 -05:00
Thomas Pelletier 07aa85ea0b Refactor to use parser state 2021-02-01 09:00:36 -05:00
Thomas Pelletier d54ad15d16 Track ABNF file 2021-02-01 09:00:21 -05:00
Thomas Pelletier abe1005d7a wip: string parsing 2021-01-30 20:31:14 -05:00
Thomas Pelletier b4bb91fc13 test 2021-01-30 09:07:55 -05:00
Vincent Serpoul c9a09d8695 Provide Tree and treeValue public aliases (#467)
Provide public aliases for Tree and treeValue to give more                                                       
control when manipulating TOML documents. This is a stop-gap
measure until we redesign the interface in v2.

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

Those should now start in discussions

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

* Update lincese year

* ci: do not run coverage on master

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

Fixes #440.

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

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

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

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

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

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

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

* Remove fuzzit

The company has been acquired by GitLab and shutting down.
2020-10-11 15:31:33 -04:00
Allen f9ba08244d Do not allow T-prefix on local dates (#446)
Fixes #442
2020-10-09 10:55:11 -04:00
Thomas Pelletier e6908614ee toml.Unmarshaler supports leaf nodes (#444)
Fixes #437
2020-09-13 18:46:13 -04:00
Cameron Moore a7448fe8de Fix date lexer to only support 4-digit year (#443)
Fixes #441
2020-09-12 18:04:04 -04:00
Thomas Pelletier 65ca806488 Fix Unmarshaler call when value is missing (#439)
Fixes #431
2020-09-12 14:42:04 -04:00
Cameron Moore 5c94d86029 Use strings.Builder in lexer (#438)
Replace all string building operations in the lexer with
strings.Builder. Doing so shows significant performance improvements.
BurntSushi still has a slight edge in CPU performance, but there's still
much work to do on memory performance.

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

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

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

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

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

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

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

Benchmarks were run from the benchmark/ directory using:

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

Fixes #432
2020-09-11 10:04:46 -04:00
Thomas Pelletier 196ce3a1f6 Support go 1.15 (#434)
Fixes #428
2020-09-10 21:39:16 -04:00
Stephen Levine 9f8f82dfe8 Fix index exception when setting empty Tree slice (#425) 2020-09-10 21:19:18 -04:00
Stephen Levine 661484ae7e Add *Tree.SetPositionPath (#426)
Signed-off-by: Stephen Levine <stephen.levine@gmail.com>
2020-07-31 23:44:50 -04:00
AllenX2018 34de94e6a8 fix issue #421 2020-07-08 19:02:44 +08:00
Allen 88263a05cc move benchmark to a seperate diectory (#420)
Fixes #418
2020-06-15 17:55:19 -04:00
jixiuf 1dbe20e76c Fix TreeFromMap on list of interfaces (#416)
Fixes #415
2020-06-13 12:27:57 -04:00
AllenX2018 05bf3807d3 fix issue#414 2020-06-11 12:10:57 +08:00
Thomas Pelletier 06838de5d2 Merge branch 'RiyaJohn-better_lists' 2020-06-01 10:18:10 -04:00
Thomas Pelletier db62263e3e Added exta tests for GetArrayPath 2020-06-01 10:16:36 -04:00
RiyaJohn 2d866e3fae fix: rm int, int32, float32 2020-05-21 22:50:23 +05:30
RiyaJohn 100799f7b7 add testcase for bool 2020-05-18 16:13:17 +05:30
RiyaJohn ecd155a62f Merge remote-tracking branch 'upstream/master' into better_lists 2020-05-18 15:54:12 +05:30
RiyaJohn bcacc71a18 feat: add GetArray() with testcases 2020-05-18 15:26:15 +05:30
Thomas Pelletier 16c9a8bdc0 Mention support to v1.0.0-rc.1 2020-05-17 17:56:44 -04:00
x-hgg-x f99d6bbca1 Fix query test
- replaced  assertArrayContainsInAnyOrder with a version which tests the exact order for more strict testing
2020-05-17 20:42:51 +08:00
Jim Tittsler 8784f9c73a Fix typos 2020-05-17 20:41:16 +08:00
x-hgg-x a60e466129 Fix index and slice expressions for query (#405)
* Fix index and slice expressions for query

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

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

Fields in anonymous structs are not supported at this point.

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

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

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

Fixes #277
2020-04-28 07:24:56 -04:00
RiyaJohn 71c324cf7b add getArray logic 2020-04-27 12:06:33 +05:30
RiyaJohn 4c840f1b8b Merge remote-tracking branch 'upstream/master' 2020-04-27 12:02:06 +05:30
Oncilla d1e0fc37ce marshal: do not encode embedded structs as sub-table (#368)
Currently, the marshalling code encodes the embedded structs as sub-tables.
This is a bit unexpected, as it differs from what encoding/json does in
that case: https://play.golang.org/p/KDPaGtrijV1

Unmarshalling code handles this scenario gracefully.

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

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

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

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

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

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

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

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

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-20 21:21:26 -05:00
dependabot-preview[bot] c4efb7477c Bump gopkg.in/yaml.v2 from 2.2.4 to 2.2.5 (#322) 2019-11-06 15:49:39 +00:00
99 changed files with 13011 additions and 12600 deletions
+3
View File
@@ -0,0 +1,3 @@
* text=auto
benchmark/benchmark.toml text eol=lf
-17
View File
@@ -1,17 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+19
View File
@@ -0,0 +1,19 @@
<!--
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.
More detailed description of the decisions being made and the reasons why (if
the patch is non-trivial).
---
Paste `benchstat` results here
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/" # Location of package manifests
schedule:
interval: "daily"
+67
View File
@@ -0,0 +1,67 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master, v2 ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '26 19 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+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}"
+25
View File
@@ -0,0 +1,25 @@
name: test
on:
push:
branches:
- v2
pull_request:
branches:
- v2
jobs:
build:
strategy:
matrix:
os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest']
go: [ '1.15', '1.16' ]
runs-on: ${{ matrix.os }}
name: ${{ matrix.go }}/${{ matrix.os }}
steps:
- uses: actions/checkout@master
- name: Setup go ${{ matrix.go }}
uses: actions/setup-go@master
with:
go-version: ${{ matrix.go }}
- name: Run unit tests
run: go test -race ./...
+84
View File
@@ -0,0 +1,84 @@
[service]
golangci-lint-version = "1.39.0"
[linters-settings.wsl]
allow-assign-and-anything = true
[linters-settings.exhaustive]
default-signifies-exhaustive = true
[linters]
disable-all = true
enable = [
"asciicheck",
"bodyclose",
"cyclop",
"deadcode",
"depguard",
"dogsled",
"dupl",
"durationcheck",
"errcheck",
"errorlint",
"exhaustive",
# "exhaustivestruct",
"exportloopref",
"forbidigo",
# "forcetypeassert",
"funlen",
"gci",
# "gochecknoglobals",
"gochecknoinits",
"gocognit",
"goconst",
"gocritic",
"gocyclo",
"godot",
"godox",
# "goerr113",
"gofmt",
"gofumpt",
"goheader",
"goimports",
"golint",
"gomnd",
# "gomoddirectives",
"gomodguard",
"goprintffuncname",
"gosec",
"gosimple",
"govet",
# "ifshort",
"importas",
"ineffassign",
"lll",
"makezero",
"misspell",
"nakedret",
"nestif",
"nilerr",
# "nlreturn",
"noctx",
"nolintlint",
#"paralleltest",
"prealloc",
"predeclared",
"revive",
"rowserrcheck",
"sqlclosecheck",
"staticcheck",
"structcheck",
"stylecheck",
# "testpackage",
"thelper",
"tparallel",
"typecheck",
"unconvert",
"unparam",
"unused",
"varcheck",
"wastedassign",
"whitespace",
# "wrapcheck",
# "wsl"
]
+114 -64
View File
@@ -1,74 +1,74 @@
## Contributing
# Contributing
Thank you for your interest in go-toml! We appreciate you considering
contributing to go-toml!
The main goal is the project is to provide an easy-to-use TOML
implementation for Go that gets the job done and gets out of your way
dealing with TOML is probably not the central piece of your project.
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 dealing
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
small, is more than welcomed!
As the single maintainer of go-toml, time is scarce. All help, big or small, is
more than welcomed!
### Ask questions
## Ask questions
Any question you may have, somebody else might have it too. Always feel
free to ask them on the [issues tracker][issues-tracker]. We will try to
answer them as clearly and quickly as possible, time permitting.
Any question you may have, somebody else might have it too. Always feel free to
ask them on the [discussion board][discussions]. We will try to answer them as
clearly and quickly as possible, time permitting.
Asking questions also helps us identify areas where the documentation needs
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
ask away!
seemingly innocent question leads to the fix of a bug. Don't hesitate and ask
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. Fix a typo, clarify an interface, add an
example, anything goes!
## Improve the documentation
The documentation is present in the [README][readme] and thorough the
source code. On release, it gets updated on [GoDoc][godoc]. 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.
The best way to share your knowledge and experience with go-toml is to improve
the documentation. Fix a typo, clarify an interface, add an example, anything
goes!
### 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
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.
## Report a bug
### 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!
First, some high-level rules:
* A short proposal with some POC code is better than a lengthy piece of
text with no code. Code speaks louder than words.
* No backward-incompatible patch will be accepted unless discussed.
Sometimes it's hard, and Go's lack of versioning by default does not
help, but we try not to break people's programs unless we absolutely have
- A short proposal with some POC code is better than a lengthy piece of text
with no code. Code speaks louder than words. That being said, bigger changes
should probably start with a [discussion][discussions].
- No backward-incompatible patch will be accepted unless discussed. Sometimes
it's hard, but we try not to break people's programs unless we absolutely have
to.
* If you are writing a new feature or extending an existing one, make sure
to write some documentation.
* Bug fixes need to be accompanied with regression tests.
* New code needs to be tested.
* Your commit messages need to explain why the change is needed, even if
already included in the PR description.
- If you are writing a new feature or extending an existing one, make sure to
write some documentation.
- Bug fixes need to be accompanied with regression tests.
- New code needs to be tested.
- Your commit messages need to explain why the change is needed, even if already
included in the PR description.
It does sound like a lot, but those best practices are here to save time
overall and continuously improve the quality of the project, which is
something everyone benefits from.
It does sound like a lot, but those best practices are here to save time overall
and continuously improve the quality of the project, which is something everyone
benefits from.
#### Get started
### Get started
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.
3. [Open up a pull request][pull-request]
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
some early feedback!
#### Run the tests
### Run the tests
You can run tests for go-toml using Go's test tool: `go test ./...`.
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).
You can run tests for go-toml using Go's test tool: `go test -race ./...`.
#### 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
the code. We enforce using `go fmt` on the whole code base.
However, given GitHub's new policy to _not_ run Actions on pull requests until a
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:
* Passing CI.
* Does not introduce backward-incompatible changes (unless discussed).
* Has relevant doc changes.
* Has relevant unit tests.
- Passing CI.
- Does not introduce backward-incompatible changes (unless discussed).
- Has relevant doc changes.
- Benchstat does not show performance regression.
1. Merge using "squash and merge".
2. Make sure to edit the commit message to keep all the useful information
nice and clean.
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
release".
@@ -123,7 +173,7 @@ Checklist:
[issues-tracker]: https://github.com/pelletier/go-toml/issues
[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md
[godoc]: https://godoc.org/github.com/pelletier/go-toml
[pkg.go.dev]: https://pkg.go.dev/github.com/pelletier/go-toml
[readme]: ./README.md
[fork]: https://help.github.com/articles/fork-a-repo
[pull-request]: https://help.github.com/en/articles/creating-a-pull-request
-11
View File
@@ -1,11 +0,0 @@
FROM golang:1.12-alpine3.9 as builder
WORKDIR /go/src/github.com/pelletier/go-toml
COPY . .
ENV CGO_ENABLED=0
ENV GOOS=linux
RUN go install ./...
FROM scratch
COPY --from=builder /go/bin/tomll /usr/bin/tomll
COPY --from=builder /go/bin/tomljson /usr/bin/tomljson
COPY --from=builder /go/bin/jsontoml /usr/bin/jsontoml
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2013 - 2017 Thomas Pelletier, Eric Anderton
Copyright (c) 2013 - 2021 Thomas Pelletier, Eric Anderton
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
-5
View File
@@ -1,5 +0,0 @@
**Issue:** add link to pelletier/go-toml issue here
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).
+368 -109
View File
@@ -1,150 +1,409 @@
# go-toml
# go-toml v2
Go library for the [TOML](https://github.com/mojombo/toml) format.
Go library for the [TOML](https://toml.io/en/) format.
This library supports TOML version
[v0.5.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md)
This library supports [TOML v1.0.0](https://toml.io/en/v1.0.0).
[![GoDoc](https://godoc.org/github.com/pelletier/go-toml?status.svg)](http://godoc.org/github.com/pelletier/go-toml)
[![license](https://img.shields.io/github/license/pelletier/go-toml.svg)](https://github.com/pelletier/go-toml/blob/master/LICENSE)
[![Build Status](https://dev.azure.com/pelletierthomas/go-toml-ci/_apis/build/status/pelletier.go-toml?branchName=master)](https://dev.azure.com/pelletierthomas/go-toml-ci/_build/latest?definitionId=1&branchName=master)
[![codecov](https://codecov.io/gh/pelletier/go-toml/branch/master/graph/badge.svg)](https://codecov.io/gh/pelletier/go-toml)
[![Go Report Card](https://goreportcard.com/badge/github.com/pelletier/go-toml)](https://goreportcard.com/report/github.com/pelletier/go-toml)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml?ref=badge_shield)
## Development status
## Features
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.
Go-toml provides the following features for using data parsed from TOML documents:
If you do not need the advanced document editing features of v1, you are
encouraged to try out this version.
* Load TOML documents from files and string data
* Easily navigate TOML structure using Tree
* Mashaling and unmarshaling to and from data structures
* Line & column position data for all parsed elements
* [Query support similar to JSON-Path](query/)
* Syntax errors contain line and column numbers
[👉 Roadmap for v2](https://github.com/pelletier/go-toml/discussions/506)
[🐞 Bug Reports](https://github.com/pelletier/go-toml/issues)
[💬 Anything else](https://github.com/pelletier/go-toml/discussions)
## Documentation
Full API, examples, and implementation notes are available in the Go documentation.
[![Go Reference](https://pkg.go.dev/badge/github.com/pelletier/go-toml/v2.svg)](https://pkg.go.dev/github.com/pelletier/go-toml/v2)
## Import
```go
import "github.com/pelletier/go-toml"
import "github.com/pelletier/go-toml/v2"
```
## Usage example
## Features
Read a TOML document:
### Stdlib behavior
```go
config, _ := toml.Load(`
[postgres]
user = "pelletier"
password = "mypassword"`)
// retrieve data directly
user := config.Get("postgres.user").(string)
As much as possible, this library is designed to behave similarly as the
standard library's `encoding/json`.
// or using an intermediate object
postgresConfig := config.Get("postgres").(*toml.Tree)
password := postgresConfig.Get("password").(string)
### 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"
```
Or use Unmarshal:
[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 Postgres struct {
User string
Password string
}
type Config struct {
Postgres Postgres
}
doc := []byte(`
[Postgres]
User = "pelletier"
Password = "mypassword"`)
config := Config{}
toml.Unmarshal(doc, &config)
fmt.Println("user=", config.Postgres.User)
```
Or use a query:
```go
// use a query to gather elements without walking the tree
q, _ := query.Compile("$..[user,password]")
results := q.Execute(config)
for ii, item := range results.Values() {
fmt.Println("Query result %d: %v", ii, item)
type MyConfig struct {
Version int
Name string
Tags []string
}
```
## Documentation
### Unmarshaling
The documentation and additional examples are available at
[godoc.org](http://godoc.org/github.com/pelletier/go-toml).
[`Unmarshal`][unmarshal] reads a TOML document and fills a Go structure with its
content. For example:
## Tools
```go
doc := `
version = 2
name = "go-toml"
tags = ["go", "toml"]
`
Go-toml provides two handy command line tools:
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)
* `tomll`: Reads TOML files and lint them.
```
go install github.com/pelletier/go-toml/cmd/tomll
tomll --help
```
* `tomljson`: Reads a TOML file and outputs its JSON representation.
```
go install github.com/pelletier/go-toml/cmd/tomljson
tomljson --help
```
* `jsontoml`: Reads a JSON file and outputs a TOML representation.
```
go install github.com/pelletier/go-toml/cmd/jsontoml
jsontoml --help
```
### Docker image
Those tools are also availble as a Docker image from
[dockerhub](https://hub.docker.com/r/pelletier/go-toml). For example, to
use `tomljson`:
```
docker run -v $PWD:/workdir pelletier/go-toml tomljson /workdir/example.toml
// Output:
// version: 2
// name: go-toml
// tags: [go toml]
```
Only master (`latest`) and tagged versions are published to dockerhub. You
can build your own image as usual:
[unmarshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Unmarshal
```
docker build -t go-toml .
### 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']
```
## Contribute
[marshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Marshal
Feel free to report bugs and patches using GitHub's pull requests system on
[pelletier/go-toml](https://github.com/pelletier/go-toml). Any feedback would be
much appreciated!
## Benchmarks
### Run tests
Execution time speedup compared to other Go TOML libraries:
`go test ./...`
<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>
### Fuzzing
<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>
The script `./fuzz.sh` is available to
run [go-fuzz](https://github.com/dvyukov/go-fuzz) on go-toml.
## Migrating from v1
## Versioning
This section describes the differences between v1 and v2, with some pointers on
how to get the original behavior when possible.
Go-toml follows [Semantic Versioning](http://semver.org/). The supported version
of [TOML](https://github.com/toml-lang/toml) is indicated at the beginning of
this document. The last two major versions of Go are supported
(see [Go Release Policy](https://golang.org/doc/devel/release.html#policy)).
### 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
-167
View File
@@ -1,167 +0,0 @@
trigger:
- master
stages:
- stage: fuzzit
displayName: "Run Fuzzit"
dependsOn: []
condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master'))
jobs:
- job: submit
displayName: "Submit"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
inputs:
version: "1.13"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml
- script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml
- task: Bash@3
inputs:
filePath: './fuzzit.sh'
env:
TYPE: fuzzing
FUZZIT_API_KEY: $(FUZZIT_API_KEY)
- stage: run_checks
displayName: "Check"
dependsOn: []
jobs:
- job: fmt
displayName: "fmt"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
inputs:
version: "1.13"
- task: Go@0
displayName: "go fmt ./..."
inputs:
command: 'custom'
customCommand: 'fmt'
arguments: './...'
- job: coverage
displayName: "coverage"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
inputs:
version: "1.13"
- task: Go@0
displayName: "Generate coverage"
inputs:
command: 'test'
arguments: "-race -coverprofile=coverage.txt -covermode=atomic"
- task: Bash@3
inputs:
targetType: 'inline'
script: 'bash <(curl -s https://codecov.io/bash) -t $(CODECOV_TOKEN)'
- job: benchmark
displayName: "benchmark"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
inputs:
version: "1.13"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- task: Bash@3
inputs:
filePath: './benchmark.sh'
arguments: "master $(Build.Repository.Uri)"
- job: fuzzing
displayName: "fuzzing"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
inputs:
version: "1.13"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml
- script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml
- task: Bash@3
inputs:
filePath: './fuzzit.sh'
env:
TYPE: local-regression
- job: go_unit_tests
displayName: "unit tests"
strategy:
matrix:
linux 1.13:
goVersion: '1.13'
imageName: 'ubuntu-latest'
mac 1.13:
goVersion: '1.13'
imageName: 'macos-10.13'
windows 1.13:
goVersion: '1.13'
imageName: 'vs2017-win2016'
linux 1.12:
goVersion: '1.12'
imageName: 'ubuntu-latest'
mac 1.12:
goVersion: '1.12'
imageName: 'macos-10.13'
windows 1.12:
goVersion: '1.12'
imageName: 'vs2017-win2016'
pool:
vmImage: $(imageName)
steps:
- task: GoTool@0
displayName: "Install Go $(goVersion)"
inputs:
version: $(goVersion)
- task: Go@0
displayName: "go test ./..."
inputs:
command: 'test'
arguments: './...'
- stage: build_docker_image
displayName: "Build Docker image"
dependsOn: run_checks
jobs:
- job: build
displayName: "Build"
pool:
vmImage: ubuntu-latest
steps:
- task: Docker@2
inputs:
command: 'build'
Dockerfile: 'Dockerfile'
buildContext: '.'
addPipelineData: false
- stage: publish_docker_image
displayName: "Publish Docker image"
dependsOn: build_docker_image
condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master'))
jobs:
- job: publish
displayName: "Publish"
pool:
vmImage: ubuntu-latest
steps:
- task: Docker@2
inputs:
containerRegistry: 'DockerHub'
repository: 'pelletier/go-toml'
command: 'buildAndPush'
Dockerfile: 'Dockerfile'
buildContext: '.'
tags: 'latest'
-164
View File
@@ -1,164 +0,0 @@
{
"array": {
"key1": [
1,
2,
3
],
"key2": [
"red",
"yellow",
"green"
],
"key3": [
[
1,
2
],
[
3,
4,
5
]
],
"key4": [
[
1,
2
],
[
"a",
"b",
"c"
]
],
"key5": [
1,
2,
3
],
"key6": [
1,
2
]
},
"boolean": {
"False": false,
"True": true
},
"datetime": {
"key1": "1979-05-27T07:32:00Z",
"key2": "1979-05-27T00:32:00-07:00",
"key3": "1979-05-27T00:32:00.999999-07:00"
},
"float": {
"both": {
"key": 6.626e-34
},
"exponent": {
"key1": 5e+22,
"key2": 1000000,
"key3": -0.02
},
"fractional": {
"key1": 1,
"key2": 3.1415,
"key3": -0.01
},
"underscores": {
"key1": 9224617.445991227,
"key2": 1e+100
}
},
"fruit": [{
"name": "apple",
"physical": {
"color": "red",
"shape": "round"
},
"variety": [{
"name": "red delicious"
},
{
"name": "granny smith"
}
]
},
{
"name": "banana",
"variety": [{
"name": "plantain"
}]
}
],
"integer": {
"key1": 99,
"key2": 42,
"key3": 0,
"key4": -17,
"underscores": {
"key1": 1000,
"key2": 5349221,
"key3": 12345
}
},
"products": [{
"name": "Hammer",
"sku": 738594937
},
{},
{
"color": "gray",
"name": "Nail",
"sku": 284758393
}
],
"string": {
"basic": {
"basic": "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF."
},
"literal": {
"multiline": {
"lines": "The first newline is\ntrimmed in raw strings.\n All other whitespace\n is preserved.\n",
"regex2": "I [dw]on't need \\d{2} apples"
},
"quoted": "Tom \"Dubs\" Preston-Werner",
"regex": "\u003c\\i\\c*\\s*\u003e",
"winpath": "C:\\Users\\nodejs\\templates",
"winpath2": "\\\\ServerX\\admin$\\system32\\"
},
"multiline": {
"continued": {
"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."
},
"key1": "One\nTwo",
"key2": "One\nTwo",
"key3": "One\nTwo"
}
},
"table": {
"inline": {
"name": {
"first": "Tom",
"last": "Preston-Werner"
},
"point": {
"x": 1,
"y": 2
}
},
"key": "value",
"subtable": {
"key": "another value"
}
},
"x": {
"y": {
"z": {
"w": {}
}
}
}
}
-31
View File
@@ -1,31 +0,0 @@
#!/bin/bash
set -ex
reference_ref=${1:-master}
reference_git=${2:-.}
if ! `hash benchstat 2>/dev/null`; then
echo "Installing benchstat"
go get golang.org/x/perf/cmd/benchstat
fi
tempdir=`mktemp -d /tmp/go-toml-benchmark-XXXXXX`
ref_tempdir="${tempdir}/ref"
ref_benchmark="${ref_tempdir}/benchmark-`echo -n ${reference_ref}|tr -s '/' '-'`.txt"
local_benchmark="`pwd`/benchmark-local.txt"
echo "=== ${reference_ref} (${ref_tempdir})"
git clone ${reference_git} ${ref_tempdir} >/dev/null 2>/dev/null
pushd ${ref_tempdir} >/dev/null
git checkout ${reference_ref} >/dev/null 2>/dev/null
go test -bench=. -benchmem | tee ${ref_benchmark}
popd >/dev/null
echo ""
echo "=== local"
go test -bench=. -benchmem | tee ${local_benchmark}
echo ""
echo "=== diff"
benchstat -delta-test=none ${ref_benchmark} ${local_benchmark}
-121
View File
@@ -1,121 +0,0 @@
---
array:
key1:
- 1
- 2
- 3
key2:
- red
- yellow
- green
key3:
- - 1
- 2
- - 3
- 4
- 5
key4:
- - 1
- 2
- - a
- b
- c
key5:
- 1
- 2
- 3
key6:
- 1
- 2
boolean:
'False': false
'True': true
datetime:
key1: '1979-05-27T07:32:00Z'
key2: '1979-05-27T00:32:00-07:00'
key3: '1979-05-27T00:32:00.999999-07:00'
float:
both:
key: 6.626e-34
exponent:
key1: 5.0e+22
key2: 1000000
key3: -0.02
fractional:
key1: 1
key2: 3.1415
key3: -0.01
underscores:
key1: 9224617.445991227
key2: 1.0e+100
fruit:
- name: apple
physical:
color: red
shape: round
variety:
- name: red delicious
- name: granny smith
- name: banana
variety:
- name: plantain
integer:
key1: 99
key2: 42
key3: 0
key4: -17
underscores:
key1: 1000
key2: 5349221
key3: 12345
products:
- name: Hammer
sku: 738594937
- {}
- color: gray
name: Nail
sku: 284758393
string:
basic:
basic: "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF."
literal:
multiline:
lines: |
The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
regex2: I [dw]on't need \d{2} apples
quoted: Tom "Dubs" Preston-Werner
regex: "<\\i\\c*\\s*>"
winpath: C:\Users\nodejs\templates
winpath2: "\\\\ServerX\\admin$\\system32\\"
multiline:
continued:
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.
key1: |-
One
Two
key2: |-
One
Two
key3: |-
One
Two
table:
inline:
name:
first: Tom
last: Preston-Werner
point:
x: 1
y: 2
key: value
subtable:
key: another value
x:
y:
z:
w: {}
+80
View File
@@ -0,0 +1,80 @@
package benchmark_test
import (
"compress/gzip"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require"
)
var bench_inputs = []struct {
name string
jsonLen int
}{
// from https://gist.githubusercontent.com/feeeper/2197d6d734729625a037af1df14cf2aa/raw/2f22b120e476d897179be3c1e2483d18067aa7df/config.toml
{"config", 806507},
// converted from https://github.com/miloyip/nativejson-benchmark
{"canada", 2090234},
{"citm_catalog", 479897},
{"twitter", 428778},
{"code", 1940472},
// converted from https://raw.githubusercontent.com/mailru/easyjson/master/benchmark/example.json
{"example", 7779},
}
func TestUnmarshalDatasetCode(t *testing.T) {
for _, tc := range bench_inputs {
t.Run(tc.name, func(t *testing.T) {
buf := fixture(t, tc.name)
var v interface{}
require.NoError(t, toml.Unmarshal(buf, &v))
b, err := json.Marshal(v)
require.NoError(t, err)
require.Equal(t, len(b), tc.jsonLen)
})
}
}
func BenchmarkUnmarshalDataset(b *testing.B) {
for _, tc := range bench_inputs {
b.Run(tc.name, func(b *testing.B) {
buf := fixture(b, tc.name)
b.SetBytes(int64(len(buf)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var v interface{}
require.NoError(b, toml.Unmarshal(buf, &v))
}
})
}
}
// fixture returns the uncompressed contents of path.
func fixture(tb testing.TB, path string) []byte {
tb.Helper()
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()
gz, err := gzip.NewReader(f)
require.NoError(tb, err)
buf, err := ioutil.ReadAll(gz)
require.NoError(tb, err)
return buf
}
+1 -1
View File
@@ -186,7 +186,7 @@ key3 = 1979-05-27T00:32:00.999999-07:00
key1 = [ 1, 2, 3 ]
key2 = [ "red", "yellow", "green" ]
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
# also ignore newlines between the brackets. Terminating commas are ok before
+654
View File
@@ -0,0 +1,654 @@
package benchmark_test
import (
"bytes"
"io/ioutil"
"testing"
"time"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require"
)
func TestUnmarshalSimple(t *testing.T) {
doc := []byte(`A = "hello"`)
d := struct {
A string
}{}
err := toml.Unmarshal(doc, &d)
if err != nil {
panic(err)
}
}
func BenchmarkUnmarshal(b *testing.B) {
b.Run("SimpleDocument", func(b *testing.B) {
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++ {
d := map[string]interface{}{}
err := toml.Unmarshal(hugoFrontMatterbytes, &d)
if err != nil {
panic(err)
}
}
})
}
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 {
Table struct {
Key string
Subtable struct {
Key string
}
Inline struct {
Name struct {
First string
Last string
}
Point struct {
X int64
Y int64
}
}
}
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
}
}
}
Integer struct {
Key1 int64
Key2 int64
Key3 int64
Key4 int64
Underscores struct {
Key1 int64
Key2 int64
Key3 int64
}
}
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
}
}
Boolean struct {
True bool
False bool
}
Datetime struct {
Key1 time.Time
Key2 time.Time
Key3 time.Time
}
Array struct {
Key1 []int64
Key2 []string
Key3 [][]int64
// TODO: Key4 not supported by go-toml's Unmarshal
Key4 []interface{}
Key5 []int64
Key6 []int64
}
Products []struct {
Name string
Sku int64
Color string
}
Fruit []struct {
Name string
Physical struct {
Color string
Shape string
}
Variety []struct {
Name string
}
}
}
func TestUnmarshalReferenceFile(t *testing.T) {
bytes, err := ioutil.ReadFile("benchmark.toml")
require.NoError(t, err)
d := benchmarkDoc{}
err = toml.Unmarshal(bytes, &d)
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"
`)
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-192
View File
@@ -1,192 +0,0 @@
package toml
import (
"bytes"
"encoding/json"
"io/ioutil"
"testing"
"time"
burntsushi "github.com/BurntSushi/toml"
yaml "gopkg.in/yaml.v2"
)
type benchmarkDoc struct {
Table struct {
Key string
Subtable struct {
Key string
}
Inline struct {
Name struct {
First string
Last string
}
Point struct {
X int64
U int64
}
}
}
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
}
}
}
Integer struct {
Key1 int64
Key2 int64
Key3 int64
Key4 int64
Underscores struct {
Key1 int64
Key2 int64
Key3 int64
}
}
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
}
}
Boolean struct {
True bool
False bool
}
Datetime struct {
Key1 time.Time
Key2 time.Time
Key3 time.Time
}
Array struct {
Key1 []int64
Key2 []string
Key3 [][]int64
// TODO: Key4 not supported by go-toml's Unmarshal
Key5 []int64
Key6 []int64
}
Products []struct {
Name string
Sku int64
Color string
}
Fruit []struct {
Name string
Physical struct {
Color string
Shape string
Variety []struct {
Name string
}
}
}
}
func BenchmarkParseToml(b *testing.B) {
fileBytes, err := ioutil.ReadFile("benchmark.toml")
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := LoadReader(bytes.NewReader(fileBytes))
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkUnmarshalToml(b *testing.B) {
bytes, err := ioutil.ReadFile("benchmark.toml")
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
target := benchmarkDoc{}
err := Unmarshal(bytes, &target)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkUnmarshalBurntSushiToml(b *testing.B) {
bytes, err := ioutil.ReadFile("benchmark.toml")
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
target := benchmarkDoc{}
err := burntsushi.Unmarshal(bytes, &target)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkUnmarshalJson(b *testing.B) {
bytes, err := ioutil.ReadFile("benchmark.json")
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
target := benchmarkDoc{}
err := json.Unmarshal(bytes, &target)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkUnmarshalYaml(b *testing.B) {
bytes, err := ioutil.ReadFile("benchmark.yml")
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
target := benchmarkDoc{}
err := yaml.Unmarshal(bytes, &target)
if err != nil {
b.Fatal(err)
}
}
}
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
-82
View File
@@ -1,82 +0,0 @@
// Jsontoml reads JSON and converts to TOML.
//
// Usage:
// cat file.toml | jsontoml > file.json
// jsontoml file1.toml > file.json
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/pelletier/go-toml"
)
func main() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "jsontoml can be used in two ways:")
fmt.Fprintln(os.Stderr, "Writing to STDIN and reading from STDOUT:")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Reading from a file name:")
fmt.Fprintln(os.Stderr, " tomljson file.toml")
}
flag.Parse()
os.Exit(processMain(flag.Args(), os.Stdin, os.Stdout, os.Stderr))
}
func processMain(files []string, defaultInput io.Reader, output io.Writer, errorOutput io.Writer) int {
// read from stdin and print to stdout
inputReader := defaultInput
if len(files) > 0 {
file, err := os.Open(files[0])
if err != nil {
printError(err, errorOutput)
return -1
}
inputReader = file
defer file.Close()
}
s, err := reader(inputReader)
if err != nil {
printError(err, errorOutput)
return -1
}
io.WriteString(output, s)
return 0
}
func printError(err error, output io.Writer) {
io.WriteString(output, err.Error()+"\n")
}
func reader(r io.Reader) (string, error) {
jsonMap := make(map[string]interface{})
jsonBytes, err := ioutil.ReadAll(r)
if err != nil {
return "", err
}
err = json.Unmarshal(jsonBytes, &jsonMap)
if err != nil {
return "", err
}
tree, err := toml.TreeFromMap(jsonMap)
if err != nil {
return "", err
}
return mapToTOML(tree)
}
func mapToTOML(t *toml.Tree) (string, error) {
tomlBytes, err := t.ToTomlString()
if err != nil {
return "", err
}
return string(tomlBytes[:]), nil
}
-92
View File
@@ -1,92 +0,0 @@
package main
import (
"bytes"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
)
func expectBufferEquality(t *testing.T, name string, buffer *bytes.Buffer, expected string) {
output := buffer.String()
if output != expected {
t.Errorf("incorrect %s: \n%sexpected %s: \n%s", name, output, name, expected)
t.Log([]rune(output))
t.Log([]rune(expected))
}
}
func expectProcessMainResults(t *testing.T, input string, args []string, exitCode int, expectedOutput string, expectedError string) {
inputReader := strings.NewReader(input)
outputBuffer := new(bytes.Buffer)
errorBuffer := new(bytes.Buffer)
returnCode := processMain(args, inputReader, outputBuffer, errorBuffer)
expectBufferEquality(t, "output", outputBuffer, expectedOutput)
expectBufferEquality(t, "error", errorBuffer, expectedError)
if returnCode != exitCode {
t.Error("incorrect return code:", returnCode, "expected", exitCode)
}
}
func TestProcessMainReadFromStdin(t *testing.T) {
expectedOutput := `
[mytoml]
a = 42.0
`
input := `{
"mytoml": {
"a": 42
}
}
`
expectedError := ``
expectedExitCode := 0
expectProcessMainResults(t, input, []string{}, expectedExitCode, expectedOutput, expectedError)
}
func TestProcessMainReadFromFile(t *testing.T) {
input := `{
"mytoml": {
"a": 42
}
}
`
tmpfile, err := ioutil.TempFile("", "example.json")
if err != nil {
t.Fatal(err)
}
if _, err := tmpfile.Write([]byte(input)); err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
expectedOutput := `
[mytoml]
a = 42.0
`
expectedError := ``
expectedExitCode := 0
expectProcessMainResults(t, ``, []string{tmpfile.Name()}, expectedExitCode, expectedOutput, expectedError)
}
func TestProcessMainReadFromMissingFile(t *testing.T) {
var expectedError string
if runtime.GOOS == "windows" {
expectedError = `open /this/file/does/not/exist: The system cannot find the path specified.
`
} else {
expectedError = `open /this/file/does/not/exist: no such file or directory
`
}
expectProcessMainResults(t, ``, []string{"/this/file/does/not/exist"}, -1, ``, expectedError)
}
-71
View File
@@ -1,71 +0,0 @@
// Tomljson reads TOML and converts to JSON.
//
// Usage:
// cat file.toml | tomljson > file.json
// tomljson file1.toml > file.json
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"github.com/pelletier/go-toml"
)
func main() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "tomljson can be used in two ways:")
fmt.Fprintln(os.Stderr, "Writing to STDIN and reading from STDOUT:")
fmt.Fprintln(os.Stderr, " cat file.toml | tomljson > file.json")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Reading from a file name:")
fmt.Fprintln(os.Stderr, " tomljson file.toml")
}
flag.Parse()
os.Exit(processMain(flag.Args(), os.Stdin, os.Stdout, os.Stderr))
}
func processMain(files []string, defaultInput io.Reader, output io.Writer, errorOutput io.Writer) int {
// read from stdin and print to stdout
inputReader := defaultInput
if len(files) > 0 {
var err error
inputReader, err = os.Open(files[0])
if err != nil {
printError(err, errorOutput)
return -1
}
}
s, err := reader(inputReader)
if err != nil {
printError(err, errorOutput)
return -1
}
io.WriteString(output, s+"\n")
return 0
}
func printError(err error, output io.Writer) {
io.WriteString(output, err.Error()+"\n")
}
func reader(r io.Reader) (string, error) {
tree, err := toml.LoadReader(r)
if err != nil {
return "", err
}
return mapToJSON(tree)
}
func mapToJSON(tree *toml.Tree) (string, error) {
treeMap := tree.ToMap()
bytes, err := json.MarshalIndent(treeMap, "", " ")
if err != nil {
return "", err
}
return string(bytes[:]), nil
}
-90
View File
@@ -1,90 +0,0 @@
package main
import (
"bytes"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
)
func expectBufferEquality(t *testing.T, name string, buffer *bytes.Buffer, expected string) {
output := buffer.String()
if output != expected {
t.Errorf("incorrect %s:\n%s\n\nexpected %s:\n%s", name, output, name, expected)
t.Log([]rune(output))
t.Log([]rune(expected))
}
}
func expectProcessMainResults(t *testing.T, input string, args []string, exitCode int, expectedOutput string, expectedError string) {
inputReader := strings.NewReader(input)
outputBuffer := new(bytes.Buffer)
errorBuffer := new(bytes.Buffer)
returnCode := processMain(args, inputReader, outputBuffer, errorBuffer)
expectBufferEquality(t, "output", outputBuffer, expectedOutput)
expectBufferEquality(t, "error", errorBuffer, expectedError)
if returnCode != exitCode {
t.Error("incorrect return code:", returnCode, "expected", exitCode)
}
}
func TestProcessMainReadFromStdin(t *testing.T) {
input := `
[mytoml]
a = 42`
expectedOutput := `{
"mytoml": {
"a": 42
}
}
`
expectedError := ``
expectedExitCode := 0
expectProcessMainResults(t, input, []string{}, expectedExitCode, expectedOutput, expectedError)
}
func TestProcessMainReadFromFile(t *testing.T) {
input := `
[mytoml]
a = 42`
tmpfile, err := ioutil.TempFile("", "example.toml")
if err != nil {
t.Fatal(err)
}
if _, err := tmpfile.Write([]byte(input)); err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
expectedOutput := `{
"mytoml": {
"a": 42
}
}
`
expectedError := ``
expectedExitCode := 0
expectProcessMainResults(t, ``, []string{tmpfile.Name()}, expectedExitCode, expectedOutput, expectedError)
}
func TestProcessMainReadFromMissingFile(t *testing.T) {
var expectedError string
if runtime.GOOS == "windows" {
expectedError = `open /this/file/does/not/exist: The system cannot find the path specified.
`
} else {
expectedError = `open /this/file/does/not/exist: no such file or directory
`
}
expectProcessMainResults(t, ``, []string{"/this/file/does/not/exist"}, -1, ``, expectedError)
}
-65
View File
@@ -1,65 +0,0 @@
// Tomll is a linter for TOML
//
// Usage:
// cat file.toml | tomll > file_linted.toml
// tomll file1.toml file2.toml # lint the two files in place
package main
import (
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/pelletier/go-toml"
)
func main() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "tomll can be used in two ways:")
fmt.Fprintln(os.Stderr, "Writing to STDIN and reading from STDOUT:")
fmt.Fprintln(os.Stderr, " cat file.toml | tomll > file.toml")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Reading and updating a list of files:")
fmt.Fprintln(os.Stderr, " tomll a.toml b.toml c.toml")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "When given a list of files, tomll will modify all files in place without asking.")
}
flag.Parse()
// read from stdin and print to stdout
if flag.NArg() == 0 {
s, err := lintReader(os.Stdin)
if err != nil {
io.WriteString(os.Stderr, err.Error())
os.Exit(-1)
}
io.WriteString(os.Stdout, s)
} else {
// otherwise modify a list of files
for _, filename := range flag.Args() {
s, err := lintFile(filename)
if err != nil {
io.WriteString(os.Stderr, err.Error())
os.Exit(-1)
}
ioutil.WriteFile(filename, []byte(s), 0644)
}
}
}
func lintFile(filename string) (string, error) {
tree, err := toml.LoadFile(filename)
if err != nil {
return "", err
}
return tree.String(), nil
}
func lintReader(r io.Reader) (string, error) {
tree, err := toml.LoadReader(r)
if err != nil {
return "", err
}
return tree.String(), nil
}
-219
View File
@@ -1,219 +0,0 @@
// Tomltestgen is a program that retrieves a given version of
// https://github.com/BurntSushi/toml-test and generates go code for go-toml's unit tests
// based on the test files.
//
// Usage: go run github.com/pelletier/go-toml/cmd/tomltestgen > toml_testgen_test.go
package main
import (
"archive/zip"
"bytes"
"flag"
"fmt"
"go/format"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"text/template"
"time"
)
type invalid struct {
Name string
Input string
}
type valid struct {
Name string
Input string
JsonRef string
}
type testsCollection struct {
Ref string
Timestamp string
Invalid []invalid
Valid []valid
Count int
}
const srcTemplate = "// Generated by tomltestgen for toml-test ref {{.Ref}} on {{.Timestamp}}\n" +
"package toml\n" +
" import (\n" +
" \"testing\"\n" +
")\n" +
"{{range .Invalid}}\n" +
"func TestInvalid{{.Name}}(t *testing.T) {\n" +
" input := {{.Input|gostr}}\n" +
" testgenInvalid(t, input)\n" +
"}\n" +
"{{end}}\n" +
"\n" +
"{{range .Valid}}\n" +
"func TestValid{{.Name}}(t *testing.T) {\n" +
" input := {{.Input|gostr}}\n" +
" jsonRef := {{.JsonRef|gostr}}\n" +
" testgenValid(t, input, jsonRef)\n" +
"}\n" +
"{{end}}\n"
func downloadTmpFile(url string) string {
log.Println("starting to download file from", url)
resp, err := http.Get(url)
if err != nil {
panic(err)
}
defer resp.Body.Close()
tmpfile, err := ioutil.TempFile("", "toml-test-*.zip")
if err != nil {
panic(err)
}
defer tmpfile.Close()
copiedLen, err := io.Copy(tmpfile, resp.Body)
if err != nil {
panic(err)
}
if resp.ContentLength > 0 && copiedLen != resp.ContentLength {
panic(fmt.Errorf("copied %d bytes, request body had %d", copiedLen, resp.ContentLength))
}
return tmpfile.Name()
}
func kebabToCamel(kebab string) string {
camel := ""
nextUpper := true
for _, c := range kebab {
if nextUpper {
camel += strings.ToUpper(string(c))
nextUpper = false
} else if c == '-' {
nextUpper = true
} else {
camel += string(c)
}
}
return camel
}
func readFileFromZip(f *zip.File) string {
reader, err := f.Open()
if err != nil {
panic(err)
}
defer reader.Close()
bytes, err := ioutil.ReadAll(reader)
if err != nil {
panic(err)
}
return string(bytes)
}
func templateGoStr(input string) string {
if len(input) > 0 && input[len(input)-1] == '\n' {
input = input[0 : len(input)-1]
}
if strings.Contains(input, "`") {
lines := strings.Split(input, "\n")
for idx, line := range lines {
lines[idx] = strconv.Quote(line + "\n")
}
return strings.Join(lines, " + \n")
}
return "`" + input + "`"
}
var (
ref = flag.String("r", "master", "git reference")
)
func usage() {
_, _ = fmt.Fprintf(os.Stderr, "usage: tomltestgen [flags]\n")
flag.PrintDefaults()
}
func main() {
flag.Usage = usage
flag.Parse()
url := "https://codeload.github.com/BurntSushi/toml-test/zip/" + *ref
resultFile := downloadTmpFile(url)
defer os.Remove(resultFile)
log.Println("file written to", resultFile)
zipReader, err := zip.OpenReader(resultFile)
if err != nil {
panic(err)
}
defer zipReader.Close()
collection := testsCollection{
Ref: *ref,
Timestamp: time.Now().Format(time.RFC3339),
}
zipFilesMap := map[string]*zip.File{}
for _, f := range zipReader.File {
zipFilesMap[f.Name] = f
}
testFileRegexp := regexp.MustCompile(`([^/]+/tests/(valid|invalid)/(.+))\.(toml)`)
for _, f := range zipReader.File {
groups := testFileRegexp.FindStringSubmatch(f.Name)
if len(groups) > 0 {
name := kebabToCamel(groups[3])
testType := groups[2]
log.Printf("> [%s] %s\n", testType, name)
tomlContent := readFileFromZip(f)
switch testType {
case "invalid":
collection.Invalid = append(collection.Invalid, invalid{
Name: name,
Input: tomlContent,
})
collection.Count++
case "valid":
baseFilePath := groups[1]
jsonFilePath := baseFilePath + ".json"
jsonContent := readFileFromZip(zipFilesMap[jsonFilePath])
collection.Valid = append(collection.Valid, valid{
Name: name,
Input: tomlContent,
JsonRef: jsonContent,
})
collection.Count++
default:
panic(fmt.Sprintf("unknown test type: %s", testType))
}
}
}
log.Printf("Collected %d tests from toml-test\n", collection.Count)
funcMap := template.FuncMap{
"gostr": templateGoStr,
}
t := template.Must(template.New("src").Funcs(funcMap).Parse(srcTemplate))
buf := new(bytes.Buffer)
err = t.Execute(buf, collection)
if err != nil {
panic(err)
}
outputBytes, err := format.Source(buf.Bytes())
if err != nil {
panic(err)
}
fmt.Println(string(outputBytes))
}
+322
View File
@@ -0,0 +1,322 @@
package toml
import (
"fmt"
"math"
"strconv"
"time"
)
func parseInteger(b []byte) (int64, error) {
if len(b) > 2 && b[0] == '0' {
switch b[1] {
case 'x':
return parseIntHex(b)
case 'b':
return parseIntBin(b)
case 'o':
return parseIntOct(b)
default:
panic(fmt.Errorf("invalid base '%c', should have been checked by scanIntOrFloat", b[1]))
}
}
return parseIntDec(b)
}
func parseLocalDate(b []byte) (LocalDate, error) {
// full-date = date-fullyear "-" date-month "-" date-mday
// date-fullyear = 4DIGIT
// date-month = 2DIGIT ; 01-12
// date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
var date LocalDate
if len(b) != 10 || b[4] != '-' || b[7] != '-' {
return date, newDecodeError(b, "dates are expected to have the format YYYY-MM-DD")
}
date.Year = parseDecimalDigits(b[0:4])
v := parseDecimalDigits(b[5:7])
date.Month = time.Month(v)
date.Day = parseDecimalDigits(b[8:10])
return date, nil
}
func parseDecimalDigits(b []byte) int {
v := 0
for _, c := range b {
v *= 10
v += int(c - '0')
}
return v
}
func parseDateTime(b []byte) (time.Time, error) {
// offset-date-time = full-date time-delim full-time
// full-time = partial-time time-offset
// time-offset = "Z" / time-numoffset
// time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
dt, b, err := parseLocalDateTime(b)
if err != nil {
return time.Time{}, err
}
var zone *time.Location
if len(b) == 0 {
// parser should have checked that when assigning the date time node
panic("date time should have a timezone")
}
if b[0] == 'Z' {
b = b[1:]
zone = time.UTC
} else {
const dateTimeByteLen = 6
if len(b) != dateTimeByteLen {
return time.Time{}, newDecodeError(b, "invalid date-time timezone")
}
direction := 1
if b[0] == '-' {
direction = -1
}
hours := digitsToInt(b[1:3])
minutes := digitsToInt(b[4:6])
seconds := direction * (hours*3600 + minutes*60)
zone = time.FixedZone("", seconds)
b = b[dateTimeByteLen:]
}
if len(b) > 0 {
return time.Time{}, newDecodeError(b, "extra bytes at the end of the timezone")
}
t := time.Date(
dt.Date.Year,
dt.Date.Month,
dt.Date.Day,
dt.Time.Hour,
dt.Time.Minute,
dt.Time.Second,
dt.Time.Nanosecond,
zone)
return t, nil
}
func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
var dt LocalDateTime
const localDateTimeByteMinLen = 11
if len(b) < localDateTimeByteMinLen {
return dt, nil, newDecodeError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]")
}
date, err := parseLocalDate(b[:10])
if err != nil {
return dt, nil, err
}
dt.Date = date
sep := b[10]
if sep != 'T' && sep != ' ' {
return dt, nil, newDecodeError(b[10:11], "datetime separator is expected to be T or a space")
}
t, rest, err := parseLocalTime(b[11:])
if err != nil {
return dt, nil, err
}
dt.Time = t
return dt, rest, nil
}
// 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
// remaining bytes as a timezone.
func parseLocalTime(b []byte) (LocalTime, []byte, error) {
var (
nspow = [10]int{0, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1e0}
t LocalTime
)
const localTimeByteLen = 8
if len(b) < localTimeByteLen {
return t, nil, newDecodeError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]")
}
t.Hour = parseDecimalDigits(b[0:2])
if b[2] != ':' {
return t, nil, newDecodeError(b[2:3], "expecting colon between hours and minutes")
}
t.Minute = parseDecimalDigits(b[3:5])
if b[5] != ':' {
return t, nil, newDecodeError(b[5:6], "expecting colon between minutes and seconds")
}
t.Second = parseDecimalDigits(b[6:8])
const minLengthWithFrac = 9
if len(b) >= minLengthWithFrac && b[minLengthWithFrac-1] == '.' {
frac := 0
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++
}
t.Nanosecond = frac * nspow[digits]
return t, b[9+digits:], nil
}
return t, b[8:], nil
}
//nolint:cyclop
func parseFloat(b []byte) (float64, error) {
if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' {
return math.NaN(), nil
}
cleaned, err := checkAndRemoveUnderscores(b)
if err != nil {
return 0, err
}
if cleaned[0] == '.' {
return 0, newDecodeError(b, "float cannot start with a dot")
}
if cleaned[len(cleaned)-1] == '.' {
return 0, newDecodeError(b, "float cannot end with a dot")
}
f, err := strconv.ParseFloat(string(cleaned), 64)
if err != nil {
return 0, newDecodeError(b, "unable to parse float: %w", err)
}
return f, nil
}
func parseIntHex(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscores(b[2:])
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(string(cleaned), 16, 64)
if err != nil {
return 0, newDecodeError(b, "couldn't parse hexadecimal number: %w", err)
}
return i, nil
}
func parseIntOct(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscores(b[2:])
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(string(cleaned), 8, 64)
if err != nil {
return 0, newDecodeError(b, "couldn't parse octal number: %w", err)
}
return i, nil
}
func parseIntBin(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscores(b[2:])
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(string(cleaned), 2, 64)
if err != nil {
return 0, newDecodeError(b, "couldn't parse binary number: %w", err)
}
return i, nil
}
func parseIntDec(b []byte) (int64, error) {
cleaned, err := checkAndRemoveUnderscores(b)
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(string(cleaned), 10, 64)
if err != nil {
return 0, newDecodeError(b, "couldn't parse decimal number: %w", err)
}
return i, nil
}
func checkAndRemoveUnderscores(b []byte) ([]byte, error) {
if b[0] == '_' {
return nil, newDecodeError(b[0:1], "number cannot start with underscore")
}
if b[len(b)-1] == '_' {
return nil, newDecodeError(b[len(b)-1:], "number cannot end with underscore")
}
// 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
}
+1 -22
View File
@@ -1,23 +1,2 @@
// Package toml is a TOML parser and manipulation library.
//
// This version supports the specification as described in
// https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md
//
// Marshaling
//
// Go-toml can marshal and unmarshal TOML documents from and to data
// structures.
//
// TOML document as a tree
//
// Go-toml can operate on a TOML document as a tree. Use one of the Load*
// functions to parse TOML data and obtain a Tree instance, then one of its
// methods to manipulate the tree.
//
// JSONPath-like queries
//
// The package github.com/pelletier/go-toml/query implements a system
// similar to JSONPath to quickly retrieve elements of a TOML document using a
// single expression. See the package documentation for more information.
//
// Package toml is a library to read and write TOML documents.
package toml
-106
View File
@@ -1,106 +0,0 @@
// code examples for godoc
package toml_test
import (
"fmt"
"log"
toml "github.com/pelletier/go-toml"
)
func Example_tree() {
config, err := toml.LoadFile("config.toml")
if err != nil {
fmt.Println("Error ", err.Error())
} else {
// retrieve data directly
directUser := config.Get("postgres.user").(string)
directPassword := config.Get("postgres.password").(string)
fmt.Println("User is", directUser, " and password is", directPassword)
// or using an intermediate object
configTree := config.Get("postgres").(*toml.Tree)
user := configTree.Get("user").(string)
password := configTree.Get("password").(string)
fmt.Println("User is", user, " and password is", password)
// show where elements are in the file
fmt.Printf("User position: %v\n", configTree.GetPosition("user"))
fmt.Printf("Password position: %v\n", configTree.GetPosition("password"))
}
}
func Example_unmarshal() {
type Employer struct {
Name string
Phone string
}
type Person struct {
Name string
Age int64
Employer Employer
}
document := []byte(`
name = "John"
age = 30
[employer]
name = "Company Inc."
phone = "+1 234 567 89012"
`)
person := Person{}
toml.Unmarshal(document, &person)
fmt.Println(person.Name, "is", person.Age, "and works at", person.Employer.Name)
// Output:
// John is 30 and works at Company Inc.
}
func ExampleMarshal() {
type Postgres struct {
User string `toml:"user"`
Password string `toml:"password"`
Database string `toml:"db" commented:"true" comment:"not used anymore"`
}
type Config struct {
Postgres Postgres `toml:"postgres" comment:"Postgres configuration"`
}
config := Config{Postgres{User: "pelletier", Password: "mypassword", Database: "old_database"}}
b, err := toml.Marshal(config)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
// Output:
// # Postgres configuration
// [postgres]
//
// # not used anymore
// # db = "old_database"
// password = "mypassword"
// user = "pelletier"
}
func ExampleUnmarshal() {
type Postgres struct {
User string
Password string
}
type Config struct {
Postgres Postgres
}
doc := []byte(`
[postgres]
user = "pelletier"
password = "mypassword"`)
config := Config{}
toml.Unmarshal(doc, &config)
fmt.Println("user=", config.Postgres.User)
// Output:
// user= pelletier
}
+260
View File
@@ -0,0 +1,260 @@
package toml
import (
"fmt"
"strconv"
"strings"
"github.com/pelletier/go-toml/v2/internal/danger"
)
// DecodeError represents an error encountered during the parsing or decoding
// of a TOML document.
//
// In addition to the error message, it contains the position in the document
// where it happened, as well as a human-readable representation that shows
// where the error occurred in the document.
type DecodeError struct {
message string
line int
column int
key Key
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
// DecodeError with full context.
type decodeError struct {
highlight []byte
message string
key Key // optional
}
func (de *decodeError) Error() string {
return de.message
}
func newDecodeError(highlight []byte, format string, args ...interface{}) error {
return &decodeError{
highlight: highlight,
message: fmt.Errorf(format, args...).Error(),
}
}
// Error returns the error message contained in the DecodeError.
func (e *DecodeError) Error() string {
return "toml: " + e.message
}
// String returns the human-readable contextualized error. This string is multi-line.
func (e *DecodeError) String() string {
return e.human
}
// Position returns the (line, column) pair indicating where the error
// occurred in the document. Positions are 1-indexed.
func (e *DecodeError) Position() (row int, column int) {
return e.line, e.column
}
// 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.
//
// highlight needs to be a sub-slice of document, or this function panics.
//
// The function copies all bytes used in DecodeError, so that document and
// highlight can be freely deallocated.
//nolint:funlen
func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
offset := danger.SubsliceOffset(document, de.highlight)
errMessage := de.Error()
errLine, errColumn := positionAtEnd(document[:offset])
before, after := linesOfContext(document, de.highlight, offset, 3)
var buf strings.Builder
maxLine := errLine + len(after) - 1
lineColumnWidth := len(strconv.Itoa(maxLine))
for i := len(before) - 1; i > 0; i-- {
line := errLine - i
buf.WriteString(formatLineNumber(line, lineColumnWidth))
buf.WriteString("|")
if len(before[i]) > 0 {
buf.WriteString(" ")
buf.Write(before[i])
}
buf.WriteRune('\n')
}
buf.WriteString(formatLineNumber(errLine, lineColumnWidth))
buf.WriteString("| ")
if len(before) > 0 {
buf.Write(before[0])
}
buf.Write(de.highlight)
if len(after) > 0 {
buf.Write(after[0])
}
buf.WriteRune('\n')
buf.WriteString(strings.Repeat(" ", lineColumnWidth))
buf.WriteString("| ")
if len(before) > 0 {
buf.WriteString(strings.Repeat(" ", len(before[0])))
}
buf.WriteString(strings.Repeat("~", len(de.highlight)))
if len(errMessage) > 0 {
buf.WriteString(" ")
buf.WriteString(errMessage)
}
for i := 1; i < len(after); i++ {
buf.WriteRune('\n')
line := errLine + i
buf.WriteString(formatLineNumber(line, lineColumnWidth))
buf.WriteString("|")
if len(after[i]) > 0 {
buf.WriteString(" ")
buf.Write(after[i])
}
}
return &DecodeError{
message: errMessage,
line: errLine,
column: errColumn,
key: de.key,
human: buf.String(),
}
}
func formatLineNumber(line int, width int) string {
format := "%" + strconv.Itoa(width) + "d"
return fmt.Sprintf(format, line)
}
func linesOfContext(document []byte, highlight []byte, offset int, linesAround int) ([][]byte, [][]byte) {
return beforeLines(document, offset, linesAround), afterLines(document, highlight, offset, linesAround)
}
func beforeLines(document []byte, offset int, linesAround int) [][]byte {
var beforeLines [][]byte
// Walk the document backward from the highlight to find previous lines
// of context.
rest := document[:offset]
backward:
for o := len(rest) - 1; o >= 0 && len(beforeLines) <= linesAround && len(rest) > 0; {
switch {
case rest[o] == '\n':
// handle individual lines
beforeLines = append(beforeLines, rest[o+1:])
rest = rest[:o]
o = len(rest) - 1
case o == 0:
// add the first line only if it's non-empty
beforeLines = append(beforeLines, rest)
break backward
default:
o--
}
}
return beforeLines
}
func afterLines(document []byte, highlight []byte, offset int, linesAround int) [][]byte {
var afterLines [][]byte
// Walk the document forward from the highlight to find the following
// lines of context.
rest := document[offset+len(highlight):]
forward:
for o := 0; o < len(rest) && len(afterLines) <= linesAround; {
switch {
case rest[o] == '\n':
// handle individual lines
afterLines = append(afterLines, rest[:o])
rest = rest[o+1:]
o = 0
case o == len(rest)-1 && o > 0:
// add last line only if it's non-empty
afterLines = append(afterLines, rest)
break forward
default:
o++
}
}
return afterLines
}
func positionAtEnd(b []byte) (row int, column int) {
row = 1
column = 1
for _, c := range b {
if c == '\n' {
row++
column = 1
} else {
column++
}
}
return
}
+219
View File
@@ -0,0 +1,219 @@
package toml
import (
"bytes"
"errors"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
//nolint:funlen
func TestDecodeError(t *testing.T) {
examples := []struct {
desc string
doc [3]string
msg string
expected string
}{
{
desc: "no context",
doc: [3]string{"", "morning", ""},
msg: "this is wrong",
expected: `
1| morning
| ~~~~~~~ this is wrong`,
},
{
desc: "one line",
doc: [3]string{"good ", "morning", " everyone"},
msg: "this is wrong",
expected: `
1| good morning everyone
| ~~~~~~~ this is wrong`,
},
{
desc: "exactly 3 lines",
doc: [3]string{`line1
line2
line3
before `, "highlighted", ` after
post line 1
post line 2
post line 3`},
msg: "this is wrong",
expected: `
1| line1
2| line2
3| line3
4| before highlighted after
| ~~~~~~~~~~~ this is wrong
5| post line 1
6| post line 2
7| post line 3`,
},
{
desc: "more than 3 lines",
doc: [3]string{`should not be seen1
should not be seen2
line1
line2
line3
before `, "highlighted", ` after
post line 1
post line 2
post line 3
should not be seen3
should not be seen4`},
msg: "this is wrong",
expected: `
3| line1
4| line2
5| line3
6| before highlighted after
| ~~~~~~~~~~~ this is wrong
7| post line 1
8| post line 2
9| post line 3`,
},
{
desc: "more than 10 total lines",
doc: [3]string{`should not be seen 0
should not be seen1
should not be seen2
should not be seen3
line1
line2
line3
before `, "highlighted", ` after
post line 1
post line 2
post line 3
should not be seen3
should not be seen4`},
msg: "this is wrong",
expected: `
5| line1
6| line2
7| line3
8| before highlighted after
| ~~~~~~~~~~~ this is wrong
9| post line 1
10| post line 2
11| post line 3`,
},
{
desc: "last line of more than 10",
doc: [3]string{`should not be seen
should not be seen
should not be seen
should not be seen
should not be seen
should not be seen
should not be seen
line1
line2
line3
before `, "highlighted", ``},
msg: "this is wrong",
expected: `
8| line1
9| line2
10| line3
11| before highlighted
| ~~~~~~~~~~~ this is wrong
`,
},
{
desc: "handle empty lines in the before/after blocks",
doc: [3]string{
`line1
line 2
before `, "highlighted", ` after
line 3
line 4
line 5`,
},
expected: `1| line1
2|
3| line 2
4| before highlighted after
| ~~~~~~~~~~~
5| line 3
6|
7| line 4`,
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
b := bytes.Buffer{}
b.Write([]byte(e.doc[0]))
start := b.Len()
b.Write([]byte(e.doc[1]))
end := b.Len()
b.Write([]byte(e.doc[2]))
doc := b.Bytes()
hl := doc[start:end]
err := wrapDecodeError(doc, &decodeError{
highlight: hl,
message: e.msg,
})
var derr *DecodeError
if !errors.As(err, &derr) {
t.Errorf("error not in expected format")
return
}
assert.Equal(t, strings.Trim(e.expected, "\n"), derr.String())
})
}
}
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
}
-29
View File
@@ -1,29 +0,0 @@
# This is a TOML document. Boom.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# You can indent as you please. Tabs or spaces. TOML don't care.
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
-29
View File
@@ -1,29 +0,0 @@
# This is a TOML document. Boom.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# You can indent as you please. Tabs or spaces. TOML don't care.
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
+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)
}
-31
View File
@@ -1,31 +0,0 @@
// +build gofuzz
package toml
func Fuzz(data []byte) int {
tree, err := LoadBytes(data)
if err != nil {
if tree != nil {
panic("tree must be nil if there is an error")
}
return 0
}
str, err := tree.ToTomlString()
if err != nil {
if str != "" {
panic(`str must be "" if there is an error`)
}
panic(err)
}
tree, err = Load(str)
if err != nil {
if tree != nil {
panic("tree must be nil if there is an error")
}
return 0
}
return 1
}
-15
View File
@@ -1,15 +0,0 @@
#! /bin/sh
set -eu
go get github.com/dvyukov/go-fuzz/go-fuzz
go get github.com/dvyukov/go-fuzz/go-fuzz-build
if [ ! -e toml-fuzz.zip ]; then
go-fuzz-build github.com/pelletier/go-toml
fi
rm -fr fuzz
mkdir -p fuzz/corpus
cp *.toml fuzz/corpus
go-fuzz -bin=toml-fuzz.zip -workdir=fuzz
-26
View File
@@ -1,26 +0,0 @@
#!/bin/bash
set -xe
# go-fuzz doesn't support modules yet, so ensure we do everything
# in the old style GOPATH way
export GO111MODULE="off"
# install go-fuzz
go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
# target name can only contain lower-case letters (a-z), digits (0-9) and a dash (-)
# to add another target, make sure to create it with `fuzzit create target`
# before using `fuzzit create job`
TARGET=toml-fuzzer
go-fuzz-build -libfuzzer -o ${TARGET}.a github.com/pelletier/go-toml
clang -fsanitize=fuzzer ${TARGET}.a -o ${TARGET}
# install fuzzit for talking to fuzzit.dev service
# or latest version:
# https://github.com/fuzzitdev/fuzzit/releases/latest/download/fuzzit_Linux_x86_64
wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.52/fuzzit_Linux_x86_64
chmod a+x fuzzit
# TODO: change kkowalczyk to go-toml and create toml-fuzzer target there
./fuzzit create job --type $TYPE go-toml/${TARGET} ${TARGET}
+4 -7
View File
@@ -1,9 +1,6 @@
module github.com/pelletier/go-toml
module github.com/pelletier/go-toml/v2
go 1.12
go 1.15
require (
github.com/BurntSushi/toml v0.3.1
github.com/davecgh/go-spew v1.1.1
gopkg.in/yaml.v2 v2.2.4
)
// latest (v1.7.0) doesn't have the fix for time.Time
require github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
+10 -10
View File
@@ -1,11 +1,11 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU=
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+152
View File
@@ -0,0 +1,152 @@
package ast
import (
"fmt"
"unsafe"
"github.com/pelletier/go-toml/v2/internal/danger"
)
// Iterator starts uninitialized, you need to call Next() first.
//
// For example:
//
// it := n.Children()
// for it.Next() {
// it.Node()
// }
type Iterator struct {
started bool
node *Node
}
// Next moves the iterator forward and returns true if points to a node, false
// otherwise.
func (c *Iterator) Next() bool {
if !c.started {
c.started = true
} else if c.node.Valid() {
c.node = c.node.Next()
}
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.
func (c *Iterator) Node() *Node {
return c.node
}
// Root contains a full AST.
//
// It is immutable once constructed with Builder.
type Root struct {
nodes []Node
}
// Iterator over the top level nodes.
func (r *Root) Iterator() Iterator {
it := Iterator{}
if len(r.nodes) > 0 {
it.node = &r.nodes[0]
}
return it
}
func (r *Root) at(idx Reference) *Node {
return &r.nodes[idx]
}
// Arrays have one child per element in the array.
// InlineTables have one child per key-value pair in the table.
// KeyValues have at least two children. The first one is the value. The
// rest make a potentially dotted key.
// Table and Array table have one child per element of the key they
// represent (same as KeyValue, but without the last node being the value).
// children []Node
type Node struct {
Kind Kind
Raw Range // Raw bytes from the input.
Data []byte // Node value (could be either allocated or referencing the input).
// References to other nodes, as offsets in the backing array from this
// node. References can go backward, so those can be negative.
next int // 0 if last element
child int // 0 if no child
}
type Range struct {
Offset uint32
Length uint32
}
// Next returns a copy of the next node, or an invalid Node if there is no
// next node.
func (n *Node) Next() *Node {
if n.next == 0 {
return nil
}
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
// can be accessed calling Next on the first child.
// Returns an invalid Node if there is none.
func (n *Node) Child() *Node {
if n.child == 0 {
return nil
}
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).
func (n *Node) Valid() bool {
return n != nil
}
// Key returns the child nodes making the Key on a supported node. Panics
// otherwise.
// They are guaranteed to be all be of the Kind Key. A simple key would return
// just one element.
func (n *Node) Key() Iterator {
switch n.Kind {
case KeyValue:
value := n.Child()
if !value.Valid() {
panic(fmt.Errorf("KeyValue should have at least two children"))
}
return Iterator{node: value.Next()}
case Table, ArrayTable:
return Iterator{node: n.Child()}
default:
panic(fmt.Errorf("Key() is not supported on a %s", n.Kind))
}
}
// Value returns a pointer to the value node of a KeyValue.
// Guaranteed to be non-nil.
// Panics if not called on a KeyValue node, or if the Children are malformed.
func (n *Node) Value() *Node {
assertKind(KeyValue, *n)
return n.Child()
}
// Children returns an iterator over a node's children.
func (n *Node) Children() Iterator {
return Iterator{node: n.Child()}
}
func assertKind(k Kind, n Node) {
if n.Kind != k {
panic(fmt.Errorf("method was expecting a %s, not a %s", k, n.Kind))
}
}
+51
View File
@@ -0,0 +1,51 @@
package ast
type Reference int
const InvalidReference Reference = -1
func (r Reference) Valid() bool {
return r != InvalidReference
}
type Builder struct {
tree Root
lastIdx int
}
func (b *Builder) Tree() *Root {
return &b.tree
}
func (b *Builder) NodeAt(ref Reference) *Node {
return b.tree.at(ref)
}
func (b *Builder) Reset() {
b.tree.nodes = b.tree.nodes[:0]
b.lastIdx = 0
}
func (b *Builder) Push(n Node) Reference {
b.lastIdx = len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n)
return Reference(b.lastIdx)
}
func (b *Builder) PushAndChain(n Node) Reference {
newIdx := len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n)
if b.lastIdx >= 0 {
b.tree.nodes[b.lastIdx].next = newIdx - b.lastIdx
}
b.lastIdx = newIdx
return Reference(b.lastIdx)
}
func (b *Builder) AttachChild(parent Reference, child Reference) {
b.tree.nodes[parent].child = int(child) - int(parent)
}
func (b *Builder) Chain(from Reference, to Reference) {
b.tree.nodes[from].next = int(to) - int(from)
}
+69
View File
@@ -0,0 +1,69 @@
package ast
import "fmt"
type Kind int
const (
// meta
Invalid Kind = iota
Comment
Key
// top level structures
Table
ArrayTable
KeyValue
// containers values
Array
InlineTable
// values
String
Bool
Float
Integer
LocalDate
LocalDateTime
DateTime
Time
)
func (k Kind) String() string {
switch k {
case Invalid:
return "Invalid"
case Comment:
return "Comment"
case Key:
return "Key"
case Table:
return "Table"
case ArrayTable:
return "ArrayTable"
case KeyValue:
return "KeyValue"
case Array:
return "Array"
case InlineTable:
return "InlineTable"
case String:
return "String"
case Bool:
return "Bool"
case Float:
return "Float"
case Integer:
return "Integer"
case LocalDate:
return "LocalDate"
case LocalDateTime:
return "LocalDateTime"
case DateTime:
return "DateTime"
case Time:
return "Time"
}
panic(fmt.Errorf("Kind.String() not implemented for '%d'", k))
}
+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)
}
})
}
}
@@ -0,0 +1,198 @@
package imported_tests
// Those tests have been imported from v1, but adjust to match the new
// defaults of v2.
import (
"fmt"
"testing"
"time"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require"
)
func TestDocMarshal(t *testing.T) {
type testDoc struct {
Title string `toml:"title"`
BasicLists testDocBasicLists `toml:"basic_lists"`
SubDocPtrs []*testSubDoc `toml:"subdocptrs"`
BasicMap map[string]string `toml:"basic_map"`
Subdocs testDocSubs `toml:"subdoc"`
Basics testDocBasics `toml:"basic"`
SubDocList []testSubDoc `toml:"subdoclist"`
err int `toml:"shouldntBeHere"`
unexported int `toml:"shouldntBeHere"`
Unexported2 int `toml:"-"`
}
var docData = testDoc{
Title: "TOML Marshal Testing",
unexported: 0,
Unexported2: 0,
Basics: testDocBasics{
Bool: true,
Date: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
Float32: 123.4,
Float64: 123.456782132399,
Int: 5000,
Uint: 5001,
String: &biteMe,
unexported: 0,
},
BasicLists: testDocBasicLists{
Floats: []*float32{&float1, &float2, &float3},
Bools: []bool{true, false, true},
Dates: []time.Time{
time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
time.Date(1980, 5, 27, 7, 32, 0, 0, time.UTC),
},
Ints: []int{8001, 8001, 8002},
Strings: []string{"One", "Two", "Three"},
UInts: []uint{5002, 5003},
},
BasicMap: map[string]string{
"one": "one",
"two": "two",
},
Subdocs: testDocSubs{
First: testSubDoc{"First", 0},
Second: &subdoc,
},
SubDocList: []testSubDoc{
{"List.First", 0},
{"List.Second", 0},
},
SubDocPtrs: []*testSubDoc{&subdoc},
}
marshalTestToml := `title = 'TOML Marshal Testing'
[basic_lists]
floats = [12.3, 45.6, 78.9]
bools = [true, false, true]
dates = [1979-05-27T07:32:00Z, 1980-05-27T07:32:00Z]
ints = [8001, 8001, 8002]
uints = [5002, 5003]
strings = ['One', 'Two', 'Three']
[[subdocptrs]]
name = 'Second'
[basic_map]
one = 'one'
two = 'two'
[subdoc]
[subdoc.second]
name = 'Second'
[subdoc.first]
name = 'First'
[basic]
uint = 5001
bool = true
float = 123.4
float64 = 123.456782132399
int = 5000
string = 'Bite me'
date = 1979-05-27T07:32:00Z
[[subdoclist]]
name = 'List.First'
[[subdoclist]]
name = 'List.Second'
`
result, err := toml.Marshal(docData)
require.NoError(t, err)
require.Equal(t, marshalTestToml, string(result))
}
func TestBasicMarshalQuotedKey(t *testing.T) {
result, err := toml.Marshal(quotedKeyMarshalTestData)
require.NoError(t, err)
expected := `'Z.string-àéù' = 'Hello'
'Yfloat-𝟘' = 3.5
['Xsubdoc-àéù']
String2 = 'One'
[['W.sublist-𝟘']]
String2 = 'Two'
[['W.sublist-𝟘']]
String2 = 'Three'
`
require.Equal(t, string(expected), string(result))
}
func TestEmptyMarshal(t *testing.T) {
type emptyMarshalTestStruct struct {
Title string `toml:"title"`
Bool bool `toml:"bool"`
Int int `toml:"int"`
String string `toml:"string"`
StringList []string `toml:"stringlist"`
Ptr *basicMarshalTestStruct `toml:"ptr"`
Map map[string]string `toml:"map"`
}
doc := emptyMarshalTestStruct{
Title: "Placeholder",
Bool: false,
Int: 0,
String: "",
StringList: []string{},
Ptr: nil,
Map: map[string]string{},
}
result, err := toml.Marshal(doc)
require.NoError(t, err)
expected := `title = 'Placeholder'
bool = false
int = 0
string = ''
stringlist = []
[map]
`
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))
})
}
File diff suppressed because it is too large Load Diff
+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
}
+1
View File
@@ -0,0 +1 @@
package tracker
-113
View File
@@ -1,113 +0,0 @@
// Parsing keys handling both bare and quoted keys.
package toml
import (
"errors"
"fmt"
"unicode"
)
// Convert the bare key group string to an array.
// The input supports double quotation and single quotation,
// but escape sequences are not supported. Lexers must unescape them beforehand.
func parseKey(key string) ([]string, error) {
runes := []rune(key)
var groups []string
if len(key) == 0 {
return nil, errors.New("empty key")
}
idx := 0
for idx < len(runes) {
for ; idx < len(runes) && isSpace(runes[idx]); idx++ {
// skip leading whitespace
}
if idx >= len(runes) {
break
}
r := runes[idx]
if isValidBareChar(r) {
// parse bare key
startIdx := idx
endIdx := -1
idx++
for idx < len(runes) {
r = runes[idx]
if isValidBareChar(r) {
idx++
} else if r == '.' {
endIdx = idx
break
} else if isSpace(r) {
endIdx = idx
for ; idx < len(runes) && isSpace(runes[idx]); idx++ {
// skip trailing whitespace
}
if idx < len(runes) && runes[idx] != '.' {
return nil, fmt.Errorf("invalid key character after whitespace: %c", runes[idx])
}
break
} else {
return nil, fmt.Errorf("invalid bare key character: %c", r)
}
}
if endIdx == -1 {
endIdx = idx
}
groups = append(groups, string(runes[startIdx:endIdx]))
} else if r == '\'' {
// parse single quoted key
idx++
startIdx := idx
for {
if idx >= len(runes) {
return nil, fmt.Errorf("unclosed single-quoted key")
}
r = runes[idx]
if r == '\'' {
groups = append(groups, string(runes[startIdx:idx]))
idx++
break
}
idx++
}
} else if r == '"' {
// parse double quoted key
idx++
startIdx := idx
for {
if idx >= len(runes) {
return nil, fmt.Errorf("unclosed double-quoted key")
}
r = runes[idx]
if r == '"' {
groups = append(groups, string(runes[startIdx:idx]))
idx++
break
}
idx++
}
} else if r == '.' {
idx++
if idx >= len(runes) {
return nil, fmt.Errorf("unexpected end of key")
}
r = runes[idx]
if !isValidBareChar(r) && r != '\'' && r != '"' && r != ' ' {
return nil, fmt.Errorf("expecting key part after dot")
}
} else {
return nil, fmt.Errorf("invalid key character: %c", r)
}
}
if len(groups) == 0 {
return nil, fmt.Errorf("empty key")
}
return groups, nil
}
func isValidBareChar(r rune) bool {
return isAlphanumeric(r) || r == '-' || unicode.IsNumber(r)
}
-79
View File
@@ -1,79 +0,0 @@
package toml
import (
"fmt"
"testing"
)
func testResult(t *testing.T, key string, expected []string) {
parsed, err := parseKey(key)
t.Logf("key=%s expected=%s parsed=%s", key, expected, parsed)
if err != nil {
t.Fatal("Unexpected error:", err)
}
if len(expected) != len(parsed) {
t.Fatal("Expected length", len(expected), "but", len(parsed), "parsed")
}
for index, expectedKey := range expected {
if expectedKey != parsed[index] {
t.Fatal("Expected", expectedKey, "at index", index, "but found", parsed[index])
}
}
}
func testError(t *testing.T, key string, expectedError string) {
res, err := parseKey(key)
if err == nil {
t.Fatalf("Expected error, but successfully parsed key %s", res)
}
if fmt.Sprintf("%s", err) != expectedError {
t.Fatalf("Expected error \"%s\", but got \"%s\".", expectedError, err)
}
}
func TestBareKeyBasic(t *testing.T) {
testResult(t, "test", []string{"test"})
}
func TestBareKeyDotted(t *testing.T) {
testResult(t, "this.is.a.key", []string{"this", "is", "a", "key"})
}
func TestDottedKeyBasic(t *testing.T) {
testResult(t, "\"a.dotted.key\"", []string{"a.dotted.key"})
}
func TestBaseKeyPound(t *testing.T) {
testError(t, "hello#world", "invalid bare key character: #")
}
func TestUnclosedSingleQuotedKey(t *testing.T) {
testError(t, "'", "unclosed single-quoted key")
}
func TestUnclosedDoubleQuotedKey(t *testing.T) {
testError(t, "\"", "unclosed double-quoted key")
}
func TestInvalidStartKeyCharacter(t *testing.T) {
testError(t, "/", "invalid key character: /")
}
func TestInvalidSpaceInKey(t *testing.T) {
testError(t, "invalid key", "invalid key character after whitespace: k")
}
func TestQuotedKeys(t *testing.T) {
testResult(t, `hello."foo".bar`, []string{"hello", "foo", "bar"})
testResult(t, `"hello!"`, []string{"hello!"})
testResult(t, `foo."ba.r".baz`, []string{"foo", "ba.r", "baz"})
// escape sequences must not be converted
testResult(t, `"hello\tworld"`, []string{`hello\tworld`})
}
func TestEmptyKey(t *testing.T) {
testError(t, ``, "empty key")
testError(t, ` `, "empty key")
testResult(t, `""`, []string{""})
}
-780
View File
@@ -1,780 +0,0 @@
// TOML lexer.
//
// Written using the principles developed by Rob Pike in
// http://www.youtube.com/watch?v=HxaD_trXwRE
package toml
import (
"bytes"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
var dateRegexp *regexp.Regexp
// Define state functions
type tomlLexStateFn func() tomlLexStateFn
// Define lexer
type tomlLexer struct {
inputIdx int
input []rune // Textual source
currentTokenStart int
currentTokenStop int
tokens []token
depth int
line int
col int
endbufferLine int
endbufferCol int
}
// Basic read operations on input
func (l *tomlLexer) read() rune {
r := l.peek()
if r == '\n' {
l.endbufferLine++
l.endbufferCol = 1
} else {
l.endbufferCol++
}
l.inputIdx++
return r
}
func (l *tomlLexer) next() rune {
r := l.read()
if r != eof {
l.currentTokenStop++
}
return r
}
func (l *tomlLexer) ignore() {
l.currentTokenStart = l.currentTokenStop
l.line = l.endbufferLine
l.col = l.endbufferCol
}
func (l *tomlLexer) skip() {
l.next()
l.ignore()
}
func (l *tomlLexer) fastForward(n int) {
for i := 0; i < n; i++ {
l.next()
}
}
func (l *tomlLexer) emitWithValue(t tokenType, value string) {
l.tokens = append(l.tokens, token{
Position: Position{l.line, l.col},
typ: t,
val: value,
})
l.ignore()
}
func (l *tomlLexer) emit(t tokenType) {
l.emitWithValue(t, string(l.input[l.currentTokenStart:l.currentTokenStop]))
}
func (l *tomlLexer) peek() rune {
if l.inputIdx >= len(l.input) {
return eof
}
return l.input[l.inputIdx]
}
func (l *tomlLexer) peekString(size int) string {
maxIdx := len(l.input)
upperIdx := l.inputIdx + size // FIXME: potential overflow
if upperIdx > maxIdx {
upperIdx = maxIdx
}
return string(l.input[l.inputIdx:upperIdx])
}
func (l *tomlLexer) follow(next string) bool {
return next == l.peekString(len(next))
}
// Error management
func (l *tomlLexer) errorf(format string, args ...interface{}) tomlLexStateFn {
l.tokens = append(l.tokens, token{
Position: Position{l.line, l.col},
typ: tokenError,
val: fmt.Sprintf(format, args...),
})
return nil
}
// State functions
func (l *tomlLexer) lexVoid() tomlLexStateFn {
for {
next := l.peek()
switch next {
case '[':
return l.lexTableKey
case '#':
return l.lexComment(l.lexVoid)
case '=':
return l.lexEqual
case '\r':
fallthrough
case '\n':
l.skip()
continue
}
if isSpace(next) {
l.skip()
}
if l.depth > 0 {
return l.lexRvalue
}
if isKeyStartChar(next) {
return l.lexKey
}
if next == eof {
l.next()
break
}
}
l.emit(tokenEOF)
return nil
}
func (l *tomlLexer) lexRvalue() tomlLexStateFn {
for {
next := l.peek()
switch next {
case '.':
return l.errorf("cannot start float with a dot")
case '=':
return l.lexEqual
case '[':
l.depth++
return l.lexLeftBracket
case ']':
l.depth--
return l.lexRightBracket
case '{':
return l.lexLeftCurlyBrace
case '}':
return l.lexRightCurlyBrace
case '#':
return l.lexComment(l.lexRvalue)
case '"':
return l.lexString
case '\'':
return l.lexLiteralString
case ',':
return l.lexComma
case '\r':
fallthrough
case '\n':
l.skip()
if l.depth == 0 {
return l.lexVoid
}
return l.lexRvalue
case '_':
return l.errorf("cannot start number with underscore")
}
if l.follow("true") {
return l.lexTrue
}
if l.follow("false") {
return l.lexFalse
}
if l.follow("inf") {
return l.lexInf
}
if l.follow("nan") {
return l.lexNan
}
if isSpace(next) {
l.skip()
continue
}
if next == eof {
l.next()
break
}
possibleDate := l.peekString(35)
dateSubmatches := dateRegexp.FindStringSubmatch(possibleDate)
if dateSubmatches != nil && dateSubmatches[0] != "" {
l.fastForward(len(dateSubmatches[0]))
if dateSubmatches[2] == "" { // no timezone information => local date
return l.lexLocalDate
}
return l.lexDate
}
if next == '+' || next == '-' || isDigit(next) {
return l.lexNumber
}
if isAlphanumeric(next) {
return l.lexKey
}
return l.errorf("no value can start with %c", next)
}
l.emit(tokenEOF)
return nil
}
func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn {
l.next()
l.emit(tokenLeftCurlyBrace)
return l.lexVoid
}
func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn {
l.next()
l.emit(tokenRightCurlyBrace)
return l.lexVoid
}
func (l *tomlLexer) lexDate() tomlLexStateFn {
l.emit(tokenDate)
return l.lexRvalue
}
func (l *tomlLexer) lexLocalDate() tomlLexStateFn {
l.emit(tokenLocalDate)
return l.lexRvalue
}
func (l *tomlLexer) lexTrue() tomlLexStateFn {
l.fastForward(4)
l.emit(tokenTrue)
return l.lexRvalue
}
func (l *tomlLexer) lexFalse() tomlLexStateFn {
l.fastForward(5)
l.emit(tokenFalse)
return l.lexRvalue
}
func (l *tomlLexer) lexInf() tomlLexStateFn {
l.fastForward(3)
l.emit(tokenInf)
return l.lexRvalue
}
func (l *tomlLexer) lexNan() tomlLexStateFn {
l.fastForward(3)
l.emit(tokenNan)
return l.lexRvalue
}
func (l *tomlLexer) lexEqual() tomlLexStateFn {
l.next()
l.emit(tokenEqual)
return l.lexRvalue
}
func (l *tomlLexer) lexComma() tomlLexStateFn {
l.next()
l.emit(tokenComma)
return l.lexRvalue
}
// Parse the key and emits its value without escape sequences.
// bare keys, basic string keys and literal string keys are supported.
func (l *tomlLexer) lexKey() tomlLexStateFn {
growingString := ""
for r := l.peek(); isKeyChar(r) || r == '\n' || r == '\r'; r = l.peek() {
if r == '"' {
l.next()
str, err := l.lexStringAsString(`"`, false, true)
if err != nil {
return l.errorf(err.Error())
}
growingString += "\"" + str + "\""
l.next()
continue
} else if r == '\'' {
l.next()
str, err := l.lexLiteralStringAsString(`'`, false)
if err != nil {
return l.errorf(err.Error())
}
growingString += "'" + str + "'"
l.next()
continue
} else if r == '\n' {
return l.errorf("keys cannot contain new lines")
} else if isSpace(r) {
break
} else if r == '.' {
// skip
} else if !isValidBareChar(r) {
return l.errorf("keys cannot contain %c character", r)
}
growingString += string(r)
l.next()
}
l.emitWithValue(tokenKey, growingString)
return l.lexVoid
}
func (l *tomlLexer) lexComment(previousState tomlLexStateFn) tomlLexStateFn {
return func() tomlLexStateFn {
for next := l.peek(); next != '\n' && next != eof; next = l.peek() {
if next == '\r' && l.follow("\r\n") {
break
}
l.next()
}
l.ignore()
return previousState
}
}
func (l *tomlLexer) lexLeftBracket() tomlLexStateFn {
l.next()
l.emit(tokenLeftBracket)
return l.lexRvalue
}
func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNewLine bool) (string, error) {
growingString := ""
if discardLeadingNewLine {
if l.follow("\r\n") {
l.skip()
l.skip()
} else if l.peek() == '\n' {
l.skip()
}
}
// find end of string
for {
if l.follow(terminator) {
return growingString, nil
}
next := l.peek()
if next == eof {
break
}
growingString += string(l.next())
}
return "", errors.New("unclosed string")
}
func (l *tomlLexer) lexLiteralString() tomlLexStateFn {
l.skip()
// handle special case for triple-quote
terminator := "'"
discardLeadingNewLine := false
if l.follow("''") {
l.skip()
l.skip()
terminator = "'''"
discardLeadingNewLine = true
}
str, err := l.lexLiteralStringAsString(terminator, discardLeadingNewLine)
if err != nil {
return l.errorf(err.Error())
}
l.emitWithValue(tokenString, str)
l.fastForward(len(terminator))
l.ignore()
return l.lexRvalue
}
// Lex a string and return the results as a string.
// Terminator is the substring indicating the end of the token.
// The resulting string does not include the terminator.
func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine, acceptNewLines bool) (string, error) {
growingString := ""
if discardLeadingNewLine {
if l.follow("\r\n") {
l.skip()
l.skip()
} else if l.peek() == '\n' {
l.skip()
}
}
for {
if l.follow(terminator) {
return growingString, nil
}
if l.follow("\\") {
l.next()
switch l.peek() {
case '\r':
fallthrough
case '\n':
fallthrough
case '\t':
fallthrough
case ' ':
// skip all whitespace chars following backslash
for strings.ContainsRune("\r\n\t ", l.peek()) {
l.next()
}
case '"':
growingString += "\""
l.next()
case 'n':
growingString += "\n"
l.next()
case 'b':
growingString += "\b"
l.next()
case 'f':
growingString += "\f"
l.next()
case '/':
growingString += "/"
l.next()
case 't':
growingString += "\t"
l.next()
case 'r':
growingString += "\r"
l.next()
case '\\':
growingString += "\\"
l.next()
case 'u':
l.next()
code := ""
for i := 0; i < 4; i++ {
c := l.peek()
if !isHexDigit(c) {
return "", errors.New("unfinished unicode escape")
}
l.next()
code = code + string(c)
}
intcode, err := strconv.ParseInt(code, 16, 32)
if err != nil {
return "", errors.New("invalid unicode escape: \\u" + code)
}
growingString += string(rune(intcode))
case 'U':
l.next()
code := ""
for i := 0; i < 8; i++ {
c := l.peek()
if !isHexDigit(c) {
return "", errors.New("unfinished unicode escape")
}
l.next()
code = code + string(c)
}
intcode, err := strconv.ParseInt(code, 16, 64)
if err != nil {
return "", errors.New("invalid unicode escape: \\U" + code)
}
growingString += string(rune(intcode))
default:
return "", errors.New("invalid escape sequence: \\" + string(l.peek()))
}
} else {
r := l.peek()
if 0x00 <= r && r <= 0x1F && !(acceptNewLines && (r == '\n' || r == '\r')) {
return "", fmt.Errorf("unescaped control character %U", r)
}
l.next()
growingString += string(r)
}
if l.peek() == eof {
break
}
}
return "", errors.New("unclosed string")
}
func (l *tomlLexer) lexString() tomlLexStateFn {
l.skip()
// handle special case for triple-quote
terminator := `"`
discardLeadingNewLine := false
acceptNewLines := false
if l.follow(`""`) {
l.skip()
l.skip()
terminator = `"""`
discardLeadingNewLine = true
acceptNewLines = true
}
str, err := l.lexStringAsString(terminator, discardLeadingNewLine, acceptNewLines)
if err != nil {
return l.errorf(err.Error())
}
l.emitWithValue(tokenString, str)
l.fastForward(len(terminator))
l.ignore()
return l.lexRvalue
}
func (l *tomlLexer) lexTableKey() tomlLexStateFn {
l.next()
if l.peek() == '[' {
// token '[[' signifies an array of tables
l.next()
l.emit(tokenDoubleLeftBracket)
return l.lexInsideTableArrayKey
}
// vanilla table key
l.emit(tokenLeftBracket)
return l.lexInsideTableKey
}
// Parse the key till "]]", but only bare keys are supported
func (l *tomlLexer) lexInsideTableArrayKey() tomlLexStateFn {
for r := l.peek(); r != eof; r = l.peek() {
switch r {
case ']':
if l.currentTokenStop > l.currentTokenStart {
l.emit(tokenKeyGroupArray)
}
l.next()
if l.peek() != ']' {
break
}
l.next()
l.emit(tokenDoubleRightBracket)
return l.lexVoid
case '[':
return l.errorf("table array key cannot contain ']'")
default:
l.next()
}
}
return l.errorf("unclosed table array key")
}
// Parse the key till "]" but only bare keys are supported
func (l *tomlLexer) lexInsideTableKey() tomlLexStateFn {
for r := l.peek(); r != eof; r = l.peek() {
switch r {
case ']':
if l.currentTokenStop > l.currentTokenStart {
l.emit(tokenKeyGroup)
}
l.next()
l.emit(tokenRightBracket)
return l.lexVoid
case '[':
return l.errorf("table key cannot contain ']'")
default:
l.next()
}
}
return l.errorf("unclosed table key")
}
func (l *tomlLexer) lexRightBracket() tomlLexStateFn {
l.next()
l.emit(tokenRightBracket)
return l.lexRvalue
}
type validRuneFn func(r rune) bool
func isValidHexRune(r rune) bool {
return r >= 'a' && r <= 'f' ||
r >= 'A' && r <= 'F' ||
r >= '0' && r <= '9' ||
r == '_'
}
func isValidOctalRune(r rune) bool {
return r >= '0' && r <= '7' || r == '_'
}
func isValidBinaryRune(r rune) bool {
return r == '0' || r == '1' || r == '_'
}
func (l *tomlLexer) lexNumber() tomlLexStateFn {
r := l.peek()
if r == '0' {
follow := l.peekString(2)
if len(follow) == 2 {
var isValidRune validRuneFn
switch follow[1] {
case 'x':
isValidRune = isValidHexRune
case 'o':
isValidRune = isValidOctalRune
case 'b':
isValidRune = isValidBinaryRune
default:
if follow[1] >= 'a' && follow[1] <= 'z' || follow[1] >= 'A' && follow[1] <= 'Z' {
return l.errorf("unknown number base: %s. possible options are x (hex) o (octal) b (binary)", string(follow[1]))
}
}
if isValidRune != nil {
l.next()
l.next()
digitSeen := false
for {
next := l.peek()
if !isValidRune(next) {
break
}
digitSeen = true
l.next()
}
if !digitSeen {
return l.errorf("number needs at least one digit")
}
l.emit(tokenInteger)
return l.lexRvalue
}
}
}
if r == '+' || r == '-' {
l.next()
if l.follow("inf") {
return l.lexInf
}
if l.follow("nan") {
return l.lexNan
}
}
pointSeen := false
expSeen := false
digitSeen := false
for {
next := l.peek()
if next == '.' {
if pointSeen {
return l.errorf("cannot have two dots in one float")
}
l.next()
if !isDigit(l.peek()) {
return l.errorf("float cannot end with a dot")
}
pointSeen = true
} else if next == 'e' || next == 'E' {
expSeen = true
l.next()
r := l.peek()
if r == '+' || r == '-' {
l.next()
}
} else if isDigit(next) {
digitSeen = true
l.next()
} else if next == '_' {
l.next()
} else {
break
}
if pointSeen && !digitSeen {
return l.errorf("cannot start float with a dot")
}
}
if !digitSeen {
return l.errorf("no digit in that number")
}
if pointSeen || expSeen {
l.emit(tokenFloat)
} else {
l.emit(tokenInteger)
}
return l.lexRvalue
}
func (l *tomlLexer) run() {
for state := l.lexVoid; state != nil; {
state = state()
}
}
func init() {
// Regexp for all date/time formats supported by TOML.
// Group 1: nano precision
// Group 2: timezone
//
// /!\ also matches the empty string
//
// Example matches:
//1979-05-27T07:32:00Z
//1979-05-27T00:32:00-07:00
//1979-05-27T00:32:00.999999-07:00
//1979-05-27 07:32:00Z
//1979-05-27 00:32:00-07:00
//1979-05-27 00:32:00.999999-07:00
//1979-05-27T07:32:00
//1979-05-27T00:32:00.999999
//1979-05-27 07:32:00
//1979-05-27 00:32:00.999999
//1979-05-27
//07:32:00
//00:32:00.999999
dateRegexp = regexp.MustCompile(`^(?:\d{1,4}-\d{2}-\d{2})?(?:[T ]?\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})?)?`)
}
// Entry point
func lexToml(inputBytes []byte) []token {
runes := bytes.Runes(inputBytes)
l := &tomlLexer{
input: runes,
tokens: make([]token, 0, 256),
line: 1,
col: 1,
endbufferLine: 1,
endbufferCol: 1,
}
l.run()
return l.tokens
}
-823
View File
@@ -1,823 +0,0 @@
package toml
import (
"reflect"
"testing"
)
func testFlow(t *testing.T, input string, expectedFlow []token) {
tokens := lexToml([]byte(input))
if !reflect.DeepEqual(tokens, expectedFlow) {
t.Fatal("Different flows. Expected\n", expectedFlow, "\nGot:\n", tokens)
}
}
func TestValidKeyGroup(t *testing.T) {
testFlow(t, "[hello world]", []token{
{Position{1, 1}, tokenLeftBracket, "["},
{Position{1, 2}, tokenKeyGroup, "hello world"},
{Position{1, 13}, tokenRightBracket, "]"},
{Position{1, 14}, tokenEOF, ""},
})
}
func TestNestedQuotedUnicodeKeyGroup(t *testing.T) {
testFlow(t, `[ j . "ʞ" . l ]`, []token{
{Position{1, 1}, tokenLeftBracket, "["},
{Position{1, 2}, tokenKeyGroup, ` j . "ʞ" . l `},
{Position{1, 15}, tokenRightBracket, "]"},
{Position{1, 16}, tokenEOF, ""},
})
}
func TestUnclosedKeyGroup(t *testing.T) {
testFlow(t, "[hello world", []token{
{Position{1, 1}, tokenLeftBracket, "["},
{Position{1, 2}, tokenError, "unclosed table key"},
})
}
func TestComment(t *testing.T) {
testFlow(t, "# blahblah", []token{
{Position{1, 11}, tokenEOF, ""},
})
}
func TestKeyGroupComment(t *testing.T) {
testFlow(t, "[hello world] # blahblah", []token{
{Position{1, 1}, tokenLeftBracket, "["},
{Position{1, 2}, tokenKeyGroup, "hello world"},
{Position{1, 13}, tokenRightBracket, "]"},
{Position{1, 25}, tokenEOF, ""},
})
}
func TestMultipleKeyGroupsComment(t *testing.T) {
testFlow(t, "[hello world] # blahblah\n[test]", []token{
{Position{1, 1}, tokenLeftBracket, "["},
{Position{1, 2}, tokenKeyGroup, "hello world"},
{Position{1, 13}, tokenRightBracket, "]"},
{Position{2, 1}, tokenLeftBracket, "["},
{Position{2, 2}, tokenKeyGroup, "test"},
{Position{2, 6}, tokenRightBracket, "]"},
{Position{2, 7}, tokenEOF, ""},
})
}
func TestSimpleWindowsCRLF(t *testing.T) {
testFlow(t, "a=4\r\nb=2", []token{
{Position{1, 1}, tokenKey, "a"},
{Position{1, 2}, tokenEqual, "="},
{Position{1, 3}, tokenInteger, "4"},
{Position{2, 1}, tokenKey, "b"},
{Position{2, 2}, tokenEqual, "="},
{Position{2, 3}, tokenInteger, "2"},
{Position{2, 4}, tokenEOF, ""},
})
}
func TestBasicKey(t *testing.T) {
testFlow(t, "hello", []token{
{Position{1, 1}, tokenKey, "hello"},
{Position{1, 6}, tokenEOF, ""},
})
}
func TestBasicKeyWithUnderscore(t *testing.T) {
testFlow(t, "hello_hello", []token{
{Position{1, 1}, tokenKey, "hello_hello"},
{Position{1, 12}, tokenEOF, ""},
})
}
func TestBasicKeyWithDash(t *testing.T) {
testFlow(t, "hello-world", []token{
{Position{1, 1}, tokenKey, "hello-world"},
{Position{1, 12}, tokenEOF, ""},
})
}
func TestBasicKeyWithUppercaseMix(t *testing.T) {
testFlow(t, "helloHELLOHello", []token{
{Position{1, 1}, tokenKey, "helloHELLOHello"},
{Position{1, 16}, tokenEOF, ""},
})
}
func TestBasicKeyWithInternationalCharacters(t *testing.T) {
testFlow(t, "héllÖ", []token{
{Position{1, 1}, tokenKey, "héllÖ"},
{Position{1, 6}, tokenEOF, ""},
})
}
func TestBasicKeyAndEqual(t *testing.T) {
testFlow(t, "hello =", []token{
{Position{1, 1}, tokenKey, "hello"},
{Position{1, 7}, tokenEqual, "="},
{Position{1, 8}, tokenEOF, ""},
})
}
func TestKeyWithSharpAndEqual(t *testing.T) {
testFlow(t, "key#name = 5", []token{
{Position{1, 1}, tokenError, "keys cannot contain # character"},
})
}
func TestKeyWithSymbolsAndEqual(t *testing.T) {
testFlow(t, "~!@$^&*()_+-`1234567890[]\\|/?><.,;:' = 5", []token{
{Position{1, 1}, tokenError, "keys cannot contain ~ character"},
})
}
func TestKeyEqualStringEscape(t *testing.T) {
testFlow(t, `foo = "hello\""`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "hello\""},
{Position{1, 16}, tokenEOF, ""},
})
}
func TestKeyEqualStringUnfinished(t *testing.T) {
testFlow(t, `foo = "bar`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenError, "unclosed string"},
})
}
func TestKeyEqualString(t *testing.T) {
testFlow(t, `foo = "bar"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "bar"},
{Position{1, 12}, tokenEOF, ""},
})
}
func TestKeyEqualTrue(t *testing.T) {
testFlow(t, "foo = true", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenTrue, "true"},
{Position{1, 11}, tokenEOF, ""},
})
}
func TestKeyEqualFalse(t *testing.T) {
testFlow(t, "foo = false", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenFalse, "false"},
{Position{1, 12}, tokenEOF, ""},
})
}
func TestArrayNestedString(t *testing.T) {
testFlow(t, `a = [ ["hello", "world"] ]`, []token{
{Position{1, 1}, tokenKey, "a"},
{Position{1, 3}, tokenEqual, "="},
{Position{1, 5}, tokenLeftBracket, "["},
{Position{1, 7}, tokenLeftBracket, "["},
{Position{1, 9}, tokenString, "hello"},
{Position{1, 15}, tokenComma, ","},
{Position{1, 18}, tokenString, "world"},
{Position{1, 24}, tokenRightBracket, "]"},
{Position{1, 26}, tokenRightBracket, "]"},
{Position{1, 27}, tokenEOF, ""},
})
}
func TestArrayNestedInts(t *testing.T) {
testFlow(t, "a = [ [42, 21], [10] ]", []token{
{Position{1, 1}, tokenKey, "a"},
{Position{1, 3}, tokenEqual, "="},
{Position{1, 5}, tokenLeftBracket, "["},
{Position{1, 7}, tokenLeftBracket, "["},
{Position{1, 8}, tokenInteger, "42"},
{Position{1, 10}, tokenComma, ","},
{Position{1, 12}, tokenInteger, "21"},
{Position{1, 14}, tokenRightBracket, "]"},
{Position{1, 15}, tokenComma, ","},
{Position{1, 17}, tokenLeftBracket, "["},
{Position{1, 18}, tokenInteger, "10"},
{Position{1, 20}, tokenRightBracket, "]"},
{Position{1, 22}, tokenRightBracket, "]"},
{Position{1, 23}, tokenEOF, ""},
})
}
func TestArrayInts(t *testing.T) {
testFlow(t, "a = [ 42, 21, 10, ]", []token{
{Position{1, 1}, tokenKey, "a"},
{Position{1, 3}, tokenEqual, "="},
{Position{1, 5}, tokenLeftBracket, "["},
{Position{1, 7}, tokenInteger, "42"},
{Position{1, 9}, tokenComma, ","},
{Position{1, 11}, tokenInteger, "21"},
{Position{1, 13}, tokenComma, ","},
{Position{1, 15}, tokenInteger, "10"},
{Position{1, 17}, tokenComma, ","},
{Position{1, 19}, tokenRightBracket, "]"},
{Position{1, 20}, tokenEOF, ""},
})
}
func TestMultilineArrayComments(t *testing.T) {
testFlow(t, "a = [1, # wow\n2, # such items\n3, # so array\n]", []token{
{Position{1, 1}, tokenKey, "a"},
{Position{1, 3}, tokenEqual, "="},
{Position{1, 5}, tokenLeftBracket, "["},
{Position{1, 6}, tokenInteger, "1"},
{Position{1, 7}, tokenComma, ","},
{Position{2, 1}, tokenInteger, "2"},
{Position{2, 2}, tokenComma, ","},
{Position{3, 1}, tokenInteger, "3"},
{Position{3, 2}, tokenComma, ","},
{Position{4, 1}, tokenRightBracket, "]"},
{Position{4, 2}, tokenEOF, ""},
})
}
func TestNestedArraysComment(t *testing.T) {
toml := `
someArray = [
# does not work
["entry1"]
]`
testFlow(t, toml, []token{
{Position{2, 1}, tokenKey, "someArray"},
{Position{2, 11}, tokenEqual, "="},
{Position{2, 13}, tokenLeftBracket, "["},
{Position{4, 1}, tokenLeftBracket, "["},
{Position{4, 3}, tokenString, "entry1"},
{Position{4, 10}, tokenRightBracket, "]"},
{Position{5, 1}, tokenRightBracket, "]"},
{Position{5, 2}, tokenEOF, ""},
})
}
func TestKeyEqualArrayBools(t *testing.T) {
testFlow(t, "foo = [true, false, true]", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftBracket, "["},
{Position{1, 8}, tokenTrue, "true"},
{Position{1, 12}, tokenComma, ","},
{Position{1, 14}, tokenFalse, "false"},
{Position{1, 19}, tokenComma, ","},
{Position{1, 21}, tokenTrue, "true"},
{Position{1, 25}, tokenRightBracket, "]"},
{Position{1, 26}, tokenEOF, ""},
})
}
func TestKeyEqualArrayBoolsWithComments(t *testing.T) {
testFlow(t, "foo = [true, false, true] # YEAH", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftBracket, "["},
{Position{1, 8}, tokenTrue, "true"},
{Position{1, 12}, tokenComma, ","},
{Position{1, 14}, tokenFalse, "false"},
{Position{1, 19}, tokenComma, ","},
{Position{1, 21}, tokenTrue, "true"},
{Position{1, 25}, tokenRightBracket, "]"},
{Position{1, 33}, tokenEOF, ""},
})
}
func TestDateRegexp(t *testing.T) {
cases := map[string]string{
"basic": "1979-05-27T07:32:00Z",
"offset": "1979-05-27T00:32:00-07:00",
"nano precision": "1979-05-27T00:32:00.999999-07:00",
"basic-no-T": "1979-05-27 07:32:00Z",
"offset-no-T": "1979-05-27 00:32:00-07:00",
"nano precision-no-T": "1979-05-27 00:32:00.999999-07:00",
"no-tz": "1979-05-27T07:32:00",
"no-tz-nano": "1979-05-27T00:32:00.999999",
"no-tz-no-t": "1979-05-27 07:32:00",
"no-tz-no-t-nano": "1979-05-27 00:32:00.999999",
"date-no-tz": "1979-05-27",
"time-no-tz": "07:32:00",
"time-no-tz-nano": "00:32:00.999999",
}
for name, value := range cases {
if dateRegexp.FindString(value) == "" {
t.Error("failed date regexp test", name)
}
}
if dateRegexp.FindString("1979-05-27 07:32:00Z") == "" {
t.Error("space delimiter lexing")
}
}
func TestKeyEqualDate(t *testing.T) {
testFlow(t, "foo = 1979-05-27T07:32:00Z", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenDate, "1979-05-27T07:32:00Z"},
{Position{1, 27}, tokenEOF, ""},
})
testFlow(t, "foo = 1979-05-27T00:32:00-07:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenDate, "1979-05-27T00:32:00-07:00"},
{Position{1, 32}, tokenEOF, ""},
})
testFlow(t, "foo = 1979-05-27T00:32:00.999999-07:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenDate, "1979-05-27T00:32:00.999999-07:00"},
{Position{1, 39}, tokenEOF, ""},
})
testFlow(t, "foo = 1979-05-27 07:32:00Z", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenDate, "1979-05-27 07:32:00Z"},
{Position{1, 27}, tokenEOF, ""},
})
}
func TestFloatEndingWithDot(t *testing.T) {
testFlow(t, "foo = 42.", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenError, "float cannot end with a dot"},
})
}
func TestFloatWithTwoDots(t *testing.T) {
testFlow(t, "foo = 4.2.", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenError, "cannot have two dots in one float"},
})
}
func TestFloatWithExponent1(t *testing.T) {
testFlow(t, "a = 5e+22", []token{
{Position{1, 1}, tokenKey, "a"},
{Position{1, 3}, tokenEqual, "="},
{Position{1, 5}, tokenFloat, "5e+22"},
{Position{1, 10}, tokenEOF, ""},
})
}
func TestFloatWithExponent2(t *testing.T) {
testFlow(t, "a = 5E+22", []token{
{Position{1, 1}, tokenKey, "a"},
{Position{1, 3}, tokenEqual, "="},
{Position{1, 5}, tokenFloat, "5E+22"},
{Position{1, 10}, tokenEOF, ""},
})
}
func TestFloatWithExponent3(t *testing.T) {
testFlow(t, "a = -5e+22", []token{
{Position{1, 1}, tokenKey, "a"},
{Position{1, 3}, tokenEqual, "="},
{Position{1, 5}, tokenFloat, "-5e+22"},
{Position{1, 11}, tokenEOF, ""},
})
}
func TestFloatWithExponent4(t *testing.T) {
testFlow(t, "a = -5e-22", []token{
{Position{1, 1}, tokenKey, "a"},
{Position{1, 3}, tokenEqual, "="},
{Position{1, 5}, tokenFloat, "-5e-22"},
{Position{1, 11}, tokenEOF, ""},
})
}
func TestFloatWithExponent5(t *testing.T) {
testFlow(t, "a = 6.626e-34", []token{
{Position{1, 1}, tokenKey, "a"},
{Position{1, 3}, tokenEqual, "="},
{Position{1, 5}, tokenFloat, "6.626e-34"},
{Position{1, 14}, tokenEOF, ""},
})
}
func TestInvalidEsquapeSequence(t *testing.T) {
testFlow(t, `foo = "\x"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenError, "invalid escape sequence: \\x"},
})
}
func TestNestedArrays(t *testing.T) {
testFlow(t, "foo = [[[]]]", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftBracket, "["},
{Position{1, 8}, tokenLeftBracket, "["},
{Position{1, 9}, tokenLeftBracket, "["},
{Position{1, 10}, tokenRightBracket, "]"},
{Position{1, 11}, tokenRightBracket, "]"},
{Position{1, 12}, tokenRightBracket, "]"},
{Position{1, 13}, tokenEOF, ""},
})
}
func TestKeyEqualNumber(t *testing.T) {
testFlow(t, "foo = 42", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenInteger, "42"},
{Position{1, 9}, tokenEOF, ""},
})
testFlow(t, "foo = +42", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenInteger, "+42"},
{Position{1, 10}, tokenEOF, ""},
})
testFlow(t, "foo = -42", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenInteger, "-42"},
{Position{1, 10}, tokenEOF, ""},
})
testFlow(t, "foo = 4.2", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenFloat, "4.2"},
{Position{1, 10}, tokenEOF, ""},
})
testFlow(t, "foo = +4.2", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenFloat, "+4.2"},
{Position{1, 11}, tokenEOF, ""},
})
testFlow(t, "foo = -4.2", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenFloat, "-4.2"},
{Position{1, 11}, tokenEOF, ""},
})
testFlow(t, "foo = 1_000", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenInteger, "1_000"},
{Position{1, 12}, tokenEOF, ""},
})
testFlow(t, "foo = 5_349_221", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenInteger, "5_349_221"},
{Position{1, 16}, tokenEOF, ""},
})
testFlow(t, "foo = 1_2_3_4_5", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenInteger, "1_2_3_4_5"},
{Position{1, 16}, tokenEOF, ""},
})
testFlow(t, "flt8 = 9_224_617.445_991_228_313", []token{
{Position{1, 1}, tokenKey, "flt8"},
{Position{1, 6}, tokenEqual, "="},
{Position{1, 8}, tokenFloat, "9_224_617.445_991_228_313"},
{Position{1, 33}, tokenEOF, ""},
})
testFlow(t, "foo = +", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenError, "no digit in that number"},
})
}
func TestMultiline(t *testing.T) {
testFlow(t, "foo = 42\nbar=21", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenInteger, "42"},
{Position{2, 1}, tokenKey, "bar"},
{Position{2, 4}, tokenEqual, "="},
{Position{2, 5}, tokenInteger, "21"},
{Position{2, 7}, tokenEOF, ""},
})
}
func TestKeyEqualStringUnicodeEscape(t *testing.T) {
testFlow(t, `foo = "hello \u2665"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "hello ♥"},
{Position{1, 21}, tokenEOF, ""},
})
testFlow(t, `foo = "hello \U000003B4"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "hello δ"},
{Position{1, 25}, tokenEOF, ""},
})
testFlow(t, `foo = "\uabcd"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "\uabcd"},
{Position{1, 15}, tokenEOF, ""},
})
testFlow(t, `foo = "\uABCD"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "\uABCD"},
{Position{1, 15}, tokenEOF, ""},
})
testFlow(t, `foo = "\U000bcdef"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "\U000bcdef"},
{Position{1, 19}, tokenEOF, ""},
})
testFlow(t, `foo = "\U000BCDEF"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "\U000BCDEF"},
{Position{1, 19}, tokenEOF, ""},
})
testFlow(t, `foo = "\u2"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenError, "unfinished unicode escape"},
})
testFlow(t, `foo = "\U2"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenError, "unfinished unicode escape"},
})
}
func TestKeyEqualStringNoEscape(t *testing.T) {
testFlow(t, "foo = \"hello \u0002\"", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenError, "unescaped control character U+0002"},
})
testFlow(t, "foo = \"hello \u001F\"", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenError, "unescaped control character U+001F"},
})
}
func TestLiteralString(t *testing.T) {
testFlow(t, `foo = 'C:\Users\nodejs\templates'`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, `C:\Users\nodejs\templates`},
{Position{1, 34}, tokenEOF, ""},
})
testFlow(t, `foo = '\\ServerX\admin$\system32\'`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, `\\ServerX\admin$\system32\`},
{Position{1, 35}, tokenEOF, ""},
})
testFlow(t, `foo = 'Tom "Dubs" Preston-Werner'`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, `Tom "Dubs" Preston-Werner`},
{Position{1, 34}, tokenEOF, ""},
})
testFlow(t, `foo = '<\i\c*\s*>'`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, `<\i\c*\s*>`},
{Position{1, 19}, tokenEOF, ""},
})
testFlow(t, `foo = 'C:\Users\nodejs\unfinis`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenError, "unclosed string"},
})
}
func TestMultilineLiteralString(t *testing.T) {
testFlow(t, `foo = '''hello 'literal' world'''`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 10}, tokenString, `hello 'literal' world`},
{Position{1, 34}, tokenEOF, ""},
})
testFlow(t, "foo = '''\nhello\n'literal'\nworld'''", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{2, 1}, tokenString, "hello\n'literal'\nworld"},
{Position{4, 9}, tokenEOF, ""},
})
testFlow(t, "foo = '''\r\nhello\r\n'literal'\r\nworld'''", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{2, 1}, tokenString, "hello\r\n'literal'\r\nworld"},
{Position{4, 9}, tokenEOF, ""},
})
}
func TestMultilineString(t *testing.T) {
testFlow(t, `foo = """hello "literal" world"""`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 10}, tokenString, `hello "literal" world`},
{Position{1, 34}, tokenEOF, ""},
})
testFlow(t, "foo = \"\"\"\r\nhello\\\r\n\"literal\"\\\nworld\"\"\"", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{2, 1}, tokenString, "hello\"literal\"world"},
{Position{4, 9}, tokenEOF, ""},
})
testFlow(t, "foo = \"\"\"\\\n \\\n \\\n hello\\\nmultiline\\\nworld\"\"\"", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 10}, tokenString, "hellomultilineworld"},
{Position{6, 9}, tokenEOF, ""},
})
testFlow(t, "key2 = \"\"\"\nThe quick brown \\\n\n\n fox jumps over \\\n the lazy dog.\"\"\"", []token{
{Position{1, 1}, tokenKey, "key2"},
{Position{1, 6}, tokenEqual, "="},
{Position{2, 1}, tokenString, "The quick brown fox jumps over the lazy dog."},
{Position{6, 21}, tokenEOF, ""},
})
testFlow(t, "key2 = \"\"\"\\\n The quick brown \\\n fox jumps over \\\n the lazy dog.\\\n \"\"\"", []token{
{Position{1, 1}, tokenKey, "key2"},
{Position{1, 6}, tokenEqual, "="},
{Position{1, 11}, tokenString, "The quick brown fox jumps over the lazy dog."},
{Position{5, 11}, tokenEOF, ""},
})
testFlow(t, `key2 = "Roses are red\nViolets are blue"`, []token{
{Position{1, 1}, tokenKey, "key2"},
{Position{1, 6}, tokenEqual, "="},
{Position{1, 9}, tokenString, "Roses are red\nViolets are blue"},
{Position{1, 41}, tokenEOF, ""},
})
testFlow(t, "key2 = \"\"\"\nRoses are red\nViolets are blue\"\"\"", []token{
{Position{1, 1}, tokenKey, "key2"},
{Position{1, 6}, tokenEqual, "="},
{Position{2, 1}, tokenString, "Roses are red\nViolets are blue"},
{Position{3, 20}, tokenEOF, ""},
})
}
func TestUnicodeString(t *testing.T) {
testFlow(t, `foo = "hello ♥ world"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "hello ♥ world"},
{Position{1, 22}, tokenEOF, ""},
})
}
func TestEscapeInString(t *testing.T) {
testFlow(t, `foo = "\b\f\/"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "\b\f/"},
{Position{1, 15}, tokenEOF, ""},
})
}
func TestKeyGroupArray(t *testing.T) {
testFlow(t, "[[foo]]", []token{
{Position{1, 1}, tokenDoubleLeftBracket, "[["},
{Position{1, 3}, tokenKeyGroupArray, "foo"},
{Position{1, 6}, tokenDoubleRightBracket, "]]"},
{Position{1, 8}, tokenEOF, ""},
})
}
func TestQuotedKey(t *testing.T) {
testFlow(t, "\"a b\" = 42", []token{
{Position{1, 1}, tokenKey, "\"a b\""},
{Position{1, 7}, tokenEqual, "="},
{Position{1, 9}, tokenInteger, "42"},
{Position{1, 11}, tokenEOF, ""},
})
}
func TestKeyNewline(t *testing.T) {
testFlow(t, "a\n= 4", []token{
{Position{1, 1}, tokenError, "keys cannot contain new lines"},
})
}
func TestInvalidFloat(t *testing.T) {
testFlow(t, "a=7e1_", []token{
{Position{1, 1}, tokenKey, "a"},
{Position{1, 2}, tokenEqual, "="},
{Position{1, 3}, tokenFloat, "7e1_"},
{Position{1, 7}, tokenEOF, ""},
})
}
func TestLexUnknownRvalue(t *testing.T) {
testFlow(t, `a = !b`, []token{
{Position{1, 1}, tokenKey, "a"},
{Position{1, 3}, tokenEqual, "="},
{Position{1, 5}, tokenError, "no value can start with !"},
})
testFlow(t, `a = \b`, []token{
{Position{1, 1}, tokenKey, "a"},
{Position{1, 3}, tokenEqual, "="},
{Position{1, 5}, tokenError, `no value can start with \`},
})
}
func TestLexInlineTableBareKey(t *testing.T) {
testFlow(t, `foo = { bar = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "bar"},
{Position{1, 13}, tokenEqual, "="},
{Position{1, 16}, tokenString, "baz"},
{Position{1, 21}, tokenRightCurlyBrace, "}"},
{Position{1, 22}, tokenEOF, ""},
})
}
func TestLexInlineTableBareKeyDash(t *testing.T) {
testFlow(t, `foo = { -bar = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "-bar"},
{Position{1, 14}, tokenEqual, "="},
{Position{1, 17}, tokenString, "baz"},
{Position{1, 22}, tokenRightCurlyBrace, "}"},
{Position{1, 23}, tokenEOF, ""},
})
}
func TestLexInlineTableBareKeyUnderscore(t *testing.T) {
testFlow(t, `foo = { _bar = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "_bar"},
{Position{1, 14}, tokenEqual, "="},
{Position{1, 17}, tokenString, "baz"},
{Position{1, 22}, tokenRightCurlyBrace, "}"},
{Position{1, 23}, tokenEOF, ""},
})
}
func TestLexInlineTableQuotedKey(t *testing.T) {
testFlow(t, `foo = { "bar" = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "\"bar\""},
{Position{1, 15}, tokenEqual, "="},
{Position{1, 18}, tokenString, "baz"},
{Position{1, 23}, tokenRightCurlyBrace, "}"},
{Position{1, 24}, tokenEOF, ""},
})
}
func BenchmarkLexer(b *testing.B) {
sample := `title = "Hugo: A Fast and Flexible Website Generator"
baseurl = "http://gohugo.io/"
MetaDataFormat = "yaml"
pluralizeListTitles = false
[params]
description = "Documentation of Hugo, a fast and flexible static site generator built with love by spf13, bep and friends in Go"
author = "Steve Francia (spf13) and friends"
release = "0.22-DEV"
[[menu.main]]
name = "Download Hugo"
pre = "<i class='fa fa-download'></i>"
url = "https://github.com/spf13/hugo/releases"
weight = -200
`
b.ResetTimer()
for i := 0; i < b.N; i++ {
lexToml([]byte(sample))
}
}
+38 -19
View File
@@ -23,6 +23,7 @@
//
// Because they lack location information, these types do not represent unique
// moments or intervals of time. Use time.Time for that purpose.
package toml
import (
@@ -44,6 +45,7 @@ type LocalDate struct {
func LocalDateOf(t time.Time) LocalDate {
var d LocalDate
d.Year, d.Month, d.Day = t.Date()
return d
}
@@ -53,6 +55,7 @@ func ParseLocalDate(s string) (LocalDate, error) {
if err != nil {
return LocalDate{}, err
}
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:
// Unix time increases by exactly 86400 seconds per day.
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.
func (d1 LocalDate) Before(d2 LocalDate) bool {
if d1.Year != d2.Year {
return d1.Year < d2.Year
// Before reports whether d1 occurs before future date.
func (d LocalDate) Before(future LocalDate) bool {
if d.Year != future.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.
func (d1 LocalDate) After(d2 LocalDate) bool {
return d2.Before(d1)
// After reports whether d1 occurs after past date.
func (d LocalDate) After(past LocalDate) bool {
return past.Before(d)
}
// MarshalText implements the encoding.TextMarshaler interface.
@@ -122,6 +130,7 @@ func (d LocalDate) MarshalText() ([]byte, error) {
func (d *LocalDate) UnmarshalText(data []byte) error {
var err error
*d, err = ParseLocalDate(string(data))
return err
}
@@ -145,6 +154,7 @@ func LocalTimeOf(t time.Time) LocalTime {
var tm LocalTime
tm.Hour, tm.Minute, tm.Second = t.Clock()
tm.Nanosecond = t.Nanosecond()
return tm
}
@@ -158,6 +168,7 @@ func ParseLocalTime(s string) (LocalTime, error) {
if err != nil {
return LocalTime{}, err
}
return LocalTimeOf(t), nil
}
@@ -169,6 +180,7 @@ func (t LocalTime) String() string {
if t.Nanosecond == 0 {
return s
}
return s + fmt.Sprintf(".%09d", t.Nanosecond)
}
@@ -176,6 +188,7 @@ func (t LocalTime) String() string {
func (t LocalTime) IsValid() bool {
// Construct a non-zero time.
tm := time.Date(2, 2, 2, t.Hour, t.Minute, t.Second, t.Nanosecond, time.UTC)
return LocalTimeOf(tm) == t
}
@@ -190,6 +203,7 @@ func (t LocalTime) MarshalText() ([]byte, error) {
func (t *LocalTime) UnmarshalText(data []byte) error {
var err error
*t, err = ParseLocalTime(string(data))
return err
}
@@ -226,6 +240,7 @@ func ParseLocalDateTime(s string) (LocalDateTime, error) {
return LocalDateTime{}, err
}
}
return LocalDateTimeOf(t), nil
}
@@ -253,17 +268,20 @@ func (dt LocalDateTime) IsValid() bool {
//
// In panics if loc is nil.
func (dt LocalDateTime) In(loc *time.Location) time.Time {
return time.Date(dt.Date.Year, dt.Date.Month, dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc)
return time.Date(
dt.Date.Year, dt.Date.Month, dt.Date.Day,
dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc,
)
}
// Before reports whether dt1 occurs before dt2.
func (dt1 LocalDateTime) Before(dt2 LocalDateTime) bool {
return dt1.In(time.UTC).Before(dt2.In(time.UTC))
// Before reports whether dt occurs before future.
func (dt LocalDateTime) Before(future LocalDateTime) bool {
return dt.In(time.UTC).Before(future.In(time.UTC))
}
// After reports whether dt1 occurs after dt2.
func (dt1 LocalDateTime) After(dt2 LocalDateTime) bool {
return dt2.Before(dt1)
// After reports whether dt occurs after past.
func (dt LocalDateTime) After(past LocalDateTime) bool {
return past.Before(dt)
}
// MarshalText implements the encoding.TextMarshaler interface.
@@ -273,9 +291,10 @@ func (dt LocalDateTime) MarshalText() ([]byte, error) {
}
// 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 {
var err error
*dt, err = ParseLocalDateTime(string(data))
return err
}
+60 -17
View File
@@ -26,6 +26,7 @@ func cmpEqual(x, y interface{}) bool {
}
func TestDates(t *testing.T) {
for _, test := range []struct {
date LocalDate
loc *time.Location
@@ -54,6 +55,7 @@ func TestDates(t *testing.T) {
if got := test.date.String(); got != test.wantStr {
t.Errorf("%#v.String() = %q, want %q", test.date, got, test.wantStr)
}
if got := test.date.In(test.loc); !got.Equal(test.wantTime) {
t.Errorf("%#v.In(%v) = %v, want %v", test.date, test.loc, got, test.wantTime)
}
@@ -61,6 +63,7 @@ func TestDates(t *testing.T) {
}
func TestDateIsValid(t *testing.T) {
for _, test := range []struct {
date LocalDate
want bool
@@ -86,6 +89,9 @@ func TestDateIsValid(t *testing.T) {
}
func TestParseDate(t *testing.T) {
var emptyDate LocalDate
for _, test := range []struct {
str string
want LocalDate // if empty, expect an error
@@ -93,21 +99,23 @@ func TestParseDate(t *testing.T) {
{"2016-01-02", LocalDate{2016, 1, 2}},
{"2016-12-31", LocalDate{2016, 12, 31}},
{"0003-02-04", LocalDate{3, 2, 4}},
{"999-01-26", LocalDate{}},
{"", LocalDate{}},
{"2016-01-02x", LocalDate{}},
{"999-01-26", emptyDate},
{"", emptyDate},
{"2016-01-02x", emptyDate},
} {
got, err := ParseLocalDate(test.str)
if got != test.want {
t.Errorf("ParseLocalDate(%q) = %+v, want %+v", test.str, got, test.want)
}
if err != nil && test.want != (LocalDate{}) {
if err != nil && test.want != (emptyDate) {
t.Errorf("Unexpected error %v from ParseLocalDate(%q)", err, test.str)
}
}
}
func TestDateArithmetic(t *testing.T) {
for _, test := range []struct {
desc string
start LocalDate
@@ -160,6 +168,7 @@ func TestDateArithmetic(t *testing.T) {
if got := test.start.AddDays(test.days); got != test.end {
t.Errorf("[%s] %#v.AddDays(%v) = %#v, want %#v", test.desc, test.start, test.days, got, test.end)
}
if got := test.end.DaysSince(test.start); got != test.days {
t.Errorf("[%s] %#v.Sub(%#v) = %v, want %v", test.desc, test.end, test.start, got, test.days)
}
@@ -167,6 +176,7 @@ func TestDateArithmetic(t *testing.T) {
}
func TestDateBefore(t *testing.T) {
for _, test := range []struct {
d1, d2 LocalDate
want bool
@@ -183,6 +193,7 @@ func TestDateBefore(t *testing.T) {
}
func TestDateAfter(t *testing.T) {
for _, test := range []struct {
d1, d2 LocalDate
want bool
@@ -198,6 +209,7 @@ func TestDateAfter(t *testing.T) {
}
func TestTimeToString(t *testing.T) {
for _, test := range []struct {
str string
time LocalTime
@@ -212,11 +224,14 @@ func TestTimeToString(t *testing.T) {
gotTime, err := ParseLocalTime(test.str)
if err != nil {
t.Errorf("ParseLocalTime(%q): got error: %v", test.str, err)
continue
}
if gotTime != test.time {
t.Errorf("ParseLocalTime(%q) = %+v, want %+v", test.str, gotTime, test.time)
}
if test.roundTrip {
gotStr := test.time.String()
if gotStr != test.str {
@@ -227,6 +242,7 @@ func TestTimeToString(t *testing.T) {
}
func TestTimeOf(t *testing.T) {
for _, test := range []struct {
time time.Time
want LocalTime
@@ -241,6 +257,7 @@ func TestTimeOf(t *testing.T) {
}
func TestTimeIsValid(t *testing.T) {
for _, test := range []struct {
time LocalTime
want bool
@@ -265,23 +282,27 @@ func TestTimeIsValid(t *testing.T) {
}
func TestDateTimeToString(t *testing.T) {
for _, test := range []struct {
str string
dateTime LocalDateTime
roundTrip bool // ParseLocalDateTime(str).String() == str?
}{
{"2016-03-22T13:26:33", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 0}}, true},
{"2016-03-22T13:26:33.000000600", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 600}}, true},
{"2016-03-22t13:26:33", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 0}}, false},
{"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, 3, 22}, LocalTime{13, 26, 33, 600}}, true},
{"2016-03-22t13:26:33", LocalDateTime{LocalDate{2016, 3, 22}, LocalTime{13, 26, 33, 0}}, false},
} {
gotDateTime, err := ParseLocalDateTime(test.str)
if err != nil {
t.Errorf("ParseLocalDateTime(%q): got error: %v", test.str, err)
continue
}
if gotDateTime != test.dateTime {
t.Errorf("ParseLocalDateTime(%q) = %+v, want %+v", test.str, gotDateTime, test.dateTime)
}
if test.roundTrip {
gotStr := test.dateTime.String()
if gotStr != test.str {
@@ -292,6 +313,7 @@ func TestDateTimeToString(t *testing.T) {
}
func TestParseDateTimeErrors(t *testing.T) {
for _, str := range []string{
"",
"2016-03-22", // just a date
@@ -306,14 +328,19 @@ func TestParseDateTimeErrors(t *testing.T) {
}
func TestDateTimeOf(t *testing.T) {
for _, test := range []struct {
time time.Time
want LocalDateTime
}{
{time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local),
LocalDateTime{LocalDate{2014, 8, 20}, LocalTime{15, 8, 43, 1}}},
{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC),
LocalDateTime{LocalDate{1, 1, 1}, LocalTime{0, 0, 0, 0}}},
{
time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local),
LocalDateTime{LocalDate{2014, 8, 20}, LocalTime{15, 8, 43, 1}},
},
{
time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC),
LocalDateTime{LocalDate{1, 1, 1}, LocalTime{0, 0, 0, 0}},
},
} {
if got := LocalDateTimeOf(test.time); got != test.want {
t.Errorf("LocalDateTimeOf(%v) = %+v, want %+v", test.time, got, test.want)
@@ -322,6 +349,7 @@ func TestDateTimeOf(t *testing.T) {
}
func TestDateTimeIsValid(t *testing.T) {
// No need to be exhaustive here; it's just LocalDate.IsValid && LocalTime.IsValid.
for _, test := range []struct {
dt LocalDateTime
@@ -339,19 +367,22 @@ func TestDateTimeIsValid(t *testing.T) {
}
func TestDateTimeIn(t *testing.T) {
dt := LocalDateTime{LocalDate{2016, 1, 2}, LocalTime{3, 4, 5, 6}}
got := dt.In(time.UTC)
want := time.Date(2016, 1, 2, 3, 4, 5, 6, time.UTC)
if !got.Equal(want) {
if got := dt.In(time.UTC); !got.Equal(want) {
t.Errorf("got %v, want %v", got, want)
}
}
func TestDateTimeBefore(t *testing.T) {
d1 := LocalDate{2016, 12, 31}
d2 := LocalDate{2017, 1, 1}
t1 := LocalTime{5, 6, 7, 8}
t2 := LocalTime{5, 6, 7, 9}
for _, test := range []struct {
dt1, dt2 LocalDateTime
want bool
@@ -368,10 +399,12 @@ func TestDateTimeBefore(t *testing.T) {
}
func TestDateTimeAfter(t *testing.T) {
d1 := LocalDate{2016, 12, 31}
d2 := LocalDate{2017, 1, 1}
t1 := LocalTime{5, 6, 7, 8}
t2 := LocalTime{5, 6, 7, 9}
for _, test := range []struct {
dt1, dt2 LocalDateTime
want bool
@@ -388,6 +421,7 @@ func TestDateTimeAfter(t *testing.T) {
}
func TestMarshalJSON(t *testing.T) {
for _, test := range []struct {
value interface{}
want string
@@ -400,6 +434,7 @@ func TestMarshalJSON(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if got := string(bgot); got != test.want {
t.Errorf("%#v: got %s, want %s", test.value, got, test.want)
}
@@ -407,9 +442,13 @@ func TestMarshalJSON(t *testing.T) {
}
func TestUnmarshalJSON(t *testing.T) {
var d LocalDate
var tm LocalTime
var dt LocalDateTime
var (
d LocalDate
tm LocalTime
dt LocalDateTime
)
for _, test := range []struct {
data string
ptr interface{}
@@ -423,12 +462,14 @@ func TestUnmarshalJSON(t *testing.T) {
if err := json.Unmarshal([]byte(test.data), test.ptr); err != nil {
t.Fatalf("%s: %v", test.data, err)
}
if !cmpEqual(test.ptr, test.want) {
t.Errorf("%s: got %#v, want %#v", test.data, test.ptr, test.want)
}
}
for _, bad := range []string{"", `""`, `"bad"`, `"1987-04-15x"`,
for _, bad := range []string{
"", `""`, `"bad"`, `"1987-04-15x"`,
`19870415`, // a JSON number
`11987-04-15x`, // not a JSON string
@@ -436,9 +477,11 @@ func TestUnmarshalJSON(t *testing.T) {
if json.Unmarshal([]byte(bad), &d) == nil {
t.Errorf("%q, LocalDate: got nil, want error", bad)
}
if json.Unmarshal([]byte(bad), &tm) == nil {
t.Errorf("%q, LocalTime: got nil, want error", bad)
}
if json.Unmarshal([]byte(bad), &dt) == nil {
t.Errorf("%q, LocalDateTime: got nil, want error", bad)
}
-868
View File
@@ -1,868 +0,0 @@
package toml
import (
"bytes"
"errors"
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
"time"
)
const (
tagFieldName = "toml"
tagFieldComment = "comment"
tagCommented = "commented"
tagMultiline = "multiline"
tagDefault = "default"
)
type tomlOpts struct {
name string
comment string
commented bool
multiline bool
include bool
omitempty bool
defaultValue string
}
type encOpts struct {
quoteMapKeys bool
arraysOneElementPerLine bool
}
var encOptsDefaults = encOpts{
quoteMapKeys: false,
}
type annotation struct {
tag string
comment string
commented string
multiline string
defaultValue string
}
var annotationDefault = annotation{
tag: tagFieldName,
comment: tagFieldComment,
commented: tagCommented,
multiline: tagMultiline,
defaultValue: tagDefault,
}
type marshalOrder int
// Orders the Encoder can write the fields to the output stream.
const (
// Sort fields alphabetically.
OrderAlphabetical marshalOrder = iota + 1
// Preserve the order the fields are encountered. For example, the order of fields in
// a struct.
OrderPreserve
)
var timeType = reflect.TypeOf(time.Time{})
var marshalerType = reflect.TypeOf(new(Marshaler)).Elem()
var localDateType = reflect.TypeOf(LocalDate{})
var localTimeType = reflect.TypeOf(LocalTime{})
var localDateTimeType = reflect.TypeOf(LocalDateTime{})
// Check if the given marshal type maps to a Tree primitive
func isPrimitive(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
return isPrimitive(mtype.Elem())
case reflect.Bool:
return true
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return true
case reflect.Float32, reflect.Float64:
return true
case reflect.String:
return true
case reflect.Struct:
return mtype == timeType || mtype == localDateType || mtype == localDateTimeType || mtype == localTimeType || isCustomMarshaler(mtype)
default:
return false
}
}
// Check if the given marshal type maps to a Tree slice or array
func isTreeSequence(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
return isTreeSequence(mtype.Elem())
case reflect.Slice, reflect.Array:
return isTree(mtype.Elem())
default:
return false
}
}
// Check if the given marshal type maps to a non-Tree slice or array
func isOtherSequence(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
return isOtherSequence(mtype.Elem())
case reflect.Slice, reflect.Array:
return !isTreeSequence(mtype)
default:
return false
}
}
// Check if the given marshal type maps to a Tree
func isTree(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
return isTree(mtype.Elem())
case reflect.Map:
return true
case reflect.Struct:
return !isPrimitive(mtype)
default:
return false
}
}
func isCustomMarshaler(mtype reflect.Type) bool {
return mtype.Implements(marshalerType)
}
func callCustomMarshaler(mval reflect.Value) ([]byte, error) {
return mval.Interface().(Marshaler).MarshalTOML()
}
// Marshaler is the interface implemented by types that
// can marshal themselves into valid TOML.
type Marshaler interface {
MarshalTOML() ([]byte, error)
}
/*
Marshal returns the TOML encoding of v. Behavior is similar to the Go json
encoder, except that there is no concept of a Marshaler interface or MarshalTOML
function for sub-structs, and currently only definite types can be marshaled
(i.e. no `interface{}`).
The following struct annotations are supported:
toml:"Field" Overrides the field's name to output.
omitempty When set, empty values and groups are not emitted.
comment:"comment" Emits a # comment on the same line. This supports new lines.
commented:"true" Emits the value as commented.
Note that pointers are automatically assigned the "omitempty" option, as TOML
explicitly does not handle null values (saying instead the label should be
dropped).
Tree structural types and corresponding marshal types:
*Tree (*)struct, (*)map[string]interface{}
[]*Tree (*)[](*)struct, (*)[](*)map[string]interface{}
[]interface{} (as interface{}) (*)[]primitive, (*)[]([]interface{})
interface{} (*)primitive
Tree primitive types and corresponding marshal types:
uint64 uint, uint8-uint64, pointers to same
int64 int, int8-uint64, pointers to same
float64 float32, float64, pointers to same
string string, pointers to same
bool bool, pointers to same
time.LocalTime time.LocalTime{}, pointers to same
For additional flexibility, use the Encoder API.
*/
func Marshal(v interface{}) ([]byte, error) {
return NewEncoder(nil).marshal(v)
}
// Encoder writes TOML values to an output stream.
type Encoder struct {
w io.Writer
encOpts
annotation
line int
col int
order marshalOrder
}
// NewEncoder returns a new encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
w: w,
encOpts: encOptsDefaults,
annotation: annotationDefault,
line: 0,
col: 1,
order: OrderAlphabetical,
}
}
// Encode writes the TOML encoding of v to the stream.
//
// See the documentation for Marshal for details.
func (e *Encoder) Encode(v interface{}) error {
b, err := e.marshal(v)
if err != nil {
return err
}
if _, err := e.w.Write(b); err != nil {
return err
}
return nil
}
// QuoteMapKeys sets up the encoder to encode
// maps with string type keys with quoted TOML keys.
//
// This relieves the character limitations on map keys.
func (e *Encoder) QuoteMapKeys(v bool) *Encoder {
e.quoteMapKeys = v
return e
}
// ArraysWithOneElementPerLine sets up the encoder to encode arrays
// with more than one element on multiple lines instead of one.
//
// For example:
//
// A = [1,2,3]
//
// Becomes
//
// A = [
// 1,
// 2,
// 3,
// ]
func (e *Encoder) ArraysWithOneElementPerLine(v bool) *Encoder {
e.arraysOneElementPerLine = v
return e
}
// Order allows to change in which order fields will be written to the output stream.
func (e *Encoder) Order(ord marshalOrder) *Encoder {
e.order = ord
return e
}
// SetTagName allows changing default tag "toml"
func (e *Encoder) SetTagName(v string) *Encoder {
e.tag = v
return e
}
// SetTagComment allows changing default tag "comment"
func (e *Encoder) SetTagComment(v string) *Encoder {
e.comment = v
return e
}
// SetTagCommented allows changing default tag "commented"
func (e *Encoder) SetTagCommented(v string) *Encoder {
e.commented = v
return e
}
// SetTagMultiline allows changing default tag "multiline"
func (e *Encoder) SetTagMultiline(v string) *Encoder {
e.multiline = v
return e
}
func (e *Encoder) marshal(v interface{}) ([]byte, error) {
mtype := reflect.TypeOf(v)
switch mtype.Kind() {
case reflect.Struct, reflect.Map:
case reflect.Ptr:
if mtype.Elem().Kind() != reflect.Struct {
return []byte{}, errors.New("Only pointer to struct can be marshaled to TOML")
}
default:
return []byte{}, errors.New("Only a struct or map can be marshaled to TOML")
}
sval := reflect.ValueOf(v)
if isCustomMarshaler(mtype) {
return callCustomMarshaler(sval)
}
t, err := e.valueToTree(mtype, sval)
if err != nil {
return []byte{}, err
}
var buf bytes.Buffer
_, err = t.writeToOrdered(&buf, "", "", 0, e.arraysOneElementPerLine, e.order)
return buf.Bytes(), err
}
// Create next tree with a position based on Encoder.line
func (e *Encoder) nextTree() *Tree {
return newTreeWithPosition(Position{Line: e.line, Col: 1})
}
// Convert given marshal struct or map value to toml tree
func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, error) {
if mtype.Kind() == reflect.Ptr {
return e.valueToTree(mtype.Elem(), mval.Elem())
}
tval := e.nextTree()
switch mtype.Kind() {
case reflect.Struct:
for i := 0; i < mtype.NumField(); i++ {
mtypef, mvalf := mtype.Field(i), mval.Field(i)
opts := tomlOptions(mtypef, e.annotation)
if opts.include && (!opts.omitempty || !isZero(mvalf)) {
val, err := e.valueToToml(mtypef.Type, mvalf)
if err != nil {
return nil, err
}
tval.SetWithOptions(opts.name, SetOptions{
Comment: opts.comment,
Commented: opts.commented,
Multiline: opts.multiline,
}, val)
}
}
case reflect.Map:
keys := mval.MapKeys()
if e.order == OrderPreserve && len(keys) > 0 {
// Sorting []reflect.Value is not straight forward.
//
// OrderPreserve will support deterministic results when string is used
// as the key to maps.
typ := keys[0].Type()
kind := keys[0].Kind()
if kind == reflect.String {
ikeys := make([]string, len(keys))
for i := range keys {
ikeys[i] = keys[i].Interface().(string)
}
sort.Strings(ikeys)
for i := range ikeys {
keys[i] = reflect.ValueOf(ikeys[i]).Convert(typ)
}
}
}
for _, key := range keys {
mvalf := mval.MapIndex(key)
val, err := e.valueToToml(mtype.Elem(), mvalf)
if err != nil {
return nil, err
}
if e.quoteMapKeys {
keyStr, err := tomlValueStringRepresentation(key.String(), "", e.arraysOneElementPerLine)
if err != nil {
return nil, err
}
tval.SetPath([]string{keyStr}, val)
} else {
tval.Set(key.String(), val)
}
}
}
return tval, nil
}
// Convert given marshal slice to slice of Toml trees
func (e *Encoder) valueToTreeSlice(mtype reflect.Type, mval reflect.Value) ([]*Tree, error) {
tval := make([]*Tree, mval.Len(), mval.Len())
for i := 0; i < mval.Len(); i++ {
val, err := e.valueToTree(mtype.Elem(), mval.Index(i))
if err != nil {
return nil, err
}
tval[i] = val
}
return tval, nil
}
// Convert given marshal slice to slice of toml values
func (e *Encoder) valueToOtherSlice(mtype reflect.Type, mval reflect.Value) (interface{}, error) {
tval := make([]interface{}, mval.Len(), mval.Len())
for i := 0; i < mval.Len(); i++ {
val, err := e.valueToToml(mtype.Elem(), mval.Index(i))
if err != nil {
return nil, err
}
tval[i] = val
}
return tval, nil
}
// Convert given marshal value to toml value
func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface{}, error) {
e.line++
if mtype.Kind() == reflect.Ptr {
return e.valueToToml(mtype.Elem(), mval.Elem())
}
switch {
case isCustomMarshaler(mtype):
return callCustomMarshaler(mval)
case isTree(mtype):
return e.valueToTree(mtype, mval)
case isTreeSequence(mtype):
return e.valueToTreeSlice(mtype, mval)
case isOtherSequence(mtype):
return e.valueToOtherSlice(mtype, mval)
default:
switch mtype.Kind() {
case reflect.Bool:
return mval.Bool(), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if mtype.Kind() == reflect.Int64 && mtype == reflect.TypeOf(time.Duration(1)) {
return fmt.Sprint(mval), nil
}
return mval.Int(), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return mval.Uint(), nil
case reflect.Float32, reflect.Float64:
return mval.Float(), nil
case reflect.String:
return mval.String(), nil
case reflect.Struct:
return mval.Interface(), nil
default:
return nil, fmt.Errorf("Marshal can't handle %v(%v)", mtype, mtype.Kind())
}
}
}
// Unmarshal attempts to unmarshal the Tree into a Go struct pointed by v.
// Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for
// sub-structs, and only definite types can be unmarshaled.
func (t *Tree) Unmarshal(v interface{}) error {
d := Decoder{tval: t, tagName: tagFieldName}
return d.unmarshal(v)
}
// Marshal returns the TOML encoding of Tree.
// See Marshal() documentation for types mapping table.
func (t *Tree) Marshal() ([]byte, error) {
var buf bytes.Buffer
_, err := t.WriteTo(&buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Unmarshal parses the TOML-encoded data and stores the result in the value
// pointed to by v. Behavior is similar to the Go json encoder, except that there
// is no concept of an Unmarshaler interface or UnmarshalTOML function for
// sub-structs, and currently only definite types can be unmarshaled to (i.e. no
// `interface{}`).
//
// The following struct annotations are supported:
//
// toml:"Field" Overrides the field's name to map to.
// default:"foo" Provides a default value.
//
// For default values, only fields of the following types are supported:
// * string
// * bool
// * int
// * int64
// * float64
//
// See Marshal() documentation for types mapping table.
func Unmarshal(data []byte, v interface{}) error {
t, err := LoadReader(bytes.NewReader(data))
if err != nil {
return err
}
return t.Unmarshal(v)
}
// Decoder reads and decodes TOML values from an input stream.
type Decoder struct {
r io.Reader
tval *Tree
encOpts
tagName string
}
// NewDecoder returns a new decoder that reads from r.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{
r: r,
encOpts: encOptsDefaults,
tagName: tagFieldName,
}
}
// Decode reads a TOML-encoded value from it's input
// and unmarshals it in the value pointed at by v.
//
// See the documentation for Marshal for details.
func (d *Decoder) Decode(v interface{}) error {
var err error
d.tval, err = LoadReader(d.r)
if err != nil {
return err
}
return d.unmarshal(v)
}
// SetTagName allows changing default tag "toml"
func (d *Decoder) SetTagName(v string) *Decoder {
d.tagName = v
return d
}
func (d *Decoder) unmarshal(v interface{}) error {
mtype := reflect.TypeOf(v)
if mtype.Kind() != reflect.Ptr {
return errors.New("only a pointer to struct or map can be unmarshaled from TOML")
}
elem := mtype.Elem()
switch elem.Kind() {
case reflect.Struct, reflect.Map:
default:
return errors.New("only a pointer to struct or map can be unmarshaled from TOML")
}
vv := reflect.ValueOf(v).Elem()
sval, err := d.valueFromTree(elem, d.tval, &vv)
if err != nil {
return err
}
reflect.ValueOf(v).Elem().Set(sval)
return nil
}
// Convert toml tree to marshal struct or map, using marshal type. When mval1
// is non-nil, merge fields into the given value instead of allocating a new one.
func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.Value) (reflect.Value, error) {
if mtype.Kind() == reflect.Ptr {
return d.unwrapPointer(mtype, tval, mval1)
}
var mval reflect.Value
switch mtype.Kind() {
case reflect.Struct:
if mval1 != nil {
mval = *mval1
} else {
mval = reflect.New(mtype).Elem()
}
for i := 0; i < mtype.NumField(); i++ {
mtypef := mtype.Field(i)
an := annotation{tag: d.tagName}
opts := tomlOptions(mtypef, an)
if opts.include {
baseKey := opts.name
keysToTry := []string{
baseKey,
strings.ToLower(baseKey),
strings.ToTitle(baseKey),
strings.ToLower(string(baseKey[0])) + baseKey[1:],
}
found := false
for _, key := range keysToTry {
exists := tval.Has(key)
if !exists {
continue
}
val := tval.Get(key)
fval := mval.Field(i)
mvalf, err := d.valueFromToml(mtypef.Type, val, &fval)
if err != nil {
return mval, formatError(err, tval.GetPosition(key))
}
mval.Field(i).Set(mvalf)
found = true
break
}
if !found && opts.defaultValue != "" {
mvalf := mval.Field(i)
var val interface{}
var err error
switch mvalf.Kind() {
case reflect.Bool:
val, err = strconv.ParseBool(opts.defaultValue)
if err != nil {
return mval.Field(i), err
}
case reflect.Int:
val, err = strconv.Atoi(opts.defaultValue)
if err != nil {
return mval.Field(i), err
}
case reflect.String:
val = opts.defaultValue
case reflect.Int64:
val, err = strconv.ParseInt(opts.defaultValue, 10, 64)
if err != nil {
return mval.Field(i), err
}
case reflect.Float64:
val, err = strconv.ParseFloat(opts.defaultValue, 64)
if err != nil {
return mval.Field(i), err
}
default:
return mval.Field(i), fmt.Errorf("unsuported field type for default option")
}
mval.Field(i).Set(reflect.ValueOf(val))
}
// save the old behavior above and try to check anonymous structs
if !found && opts.defaultValue == "" && mtypef.Anonymous && mtypef.Type.Kind() == reflect.Struct {
v, err := d.valueFromTree(mtypef.Type, tval, nil)
if err != nil {
return v, err
}
mval.Field(i).Set(v)
}
}
}
case reflect.Map:
mval = reflect.MakeMap(mtype)
for _, key := range tval.Keys() {
// TODO: path splits key
val := tval.GetPath([]string{key})
mvalf, err := d.valueFromToml(mtype.Elem(), val, nil)
if err != nil {
return mval, formatError(err, tval.GetPosition(key))
}
mval.SetMapIndex(reflect.ValueOf(key).Convert(mtype.Key()), mvalf)
}
}
return mval, nil
}
// Convert toml value to marshal struct/map slice, using marshal type
func (d *Decoder) valueFromTreeSlice(mtype reflect.Type, tval []*Tree) (reflect.Value, error) {
mval := reflect.MakeSlice(mtype, len(tval), len(tval))
for i := 0; i < len(tval); i++ {
val, err := d.valueFromTree(mtype.Elem(), tval[i], nil)
if err != nil {
return mval, err
}
mval.Index(i).Set(val)
}
return mval, nil
}
// Convert toml value to marshal primitive slice, using marshal type
func (d *Decoder) valueFromOtherSlice(mtype reflect.Type, tval []interface{}) (reflect.Value, error) {
mval := reflect.MakeSlice(mtype, len(tval), len(tval))
for i := 0; i < len(tval); i++ {
val, err := d.valueFromToml(mtype.Elem(), tval[i], nil)
if err != nil {
return mval, err
}
mval.Index(i).Set(val)
}
return mval, nil
}
// Convert toml value to marshal value, using marshal type. When mval1 is non-nil
// and the given type is a struct value, merge fields into it.
func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}, mval1 *reflect.Value) (reflect.Value, error) {
if mtype.Kind() == reflect.Ptr {
return d.unwrapPointer(mtype, tval, mval1)
}
switch t := tval.(type) {
case *Tree:
var mval11 *reflect.Value
if mtype.Kind() == reflect.Struct {
mval11 = mval1
}
if isTree(mtype) {
return d.valueFromTree(mtype, t, mval11)
}
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a tree", tval, tval)
case []*Tree:
if isTreeSequence(mtype) {
return d.valueFromTreeSlice(mtype, t)
}
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to trees", tval, tval)
case []interface{}:
if isOtherSequence(mtype) {
return d.valueFromOtherSlice(mtype, t)
}
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a slice", tval, tval)
default:
switch mtype.Kind() {
case reflect.Bool, reflect.Struct:
val := reflect.ValueOf(tval)
switch val.Type() {
case localDateType:
localDate := val.Interface().(LocalDate)
switch mtype {
case timeType:
return reflect.ValueOf(time.Date(localDate.Year, localDate.Month, localDate.Day, 0, 0, 0, 0, time.Local)), nil
}
case localDateTimeType:
localDateTime := val.Interface().(LocalDateTime)
switch mtype {
case timeType:
return reflect.ValueOf(time.Date(
localDateTime.Date.Year,
localDateTime.Date.Month,
localDateTime.Date.Day,
localDateTime.Time.Hour,
localDateTime.Time.Minute,
localDateTime.Time.Second,
localDateTime.Time.Nanosecond,
time.Local)), nil
}
}
// if this passes for when mtype is reflect.Struct, tval is a time.LocalTime
if !val.Type().ConvertibleTo(mtype) {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
}
return val.Convert(mtype), nil
case reflect.String:
val := reflect.ValueOf(tval)
// stupidly, int64 is convertible to string. So special case this.
if !val.Type().ConvertibleTo(mtype) || val.Kind() == reflect.Int64 {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
}
return val.Convert(mtype), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
val := reflect.ValueOf(tval)
if mtype.Kind() == reflect.Int64 && mtype == reflect.TypeOf(time.Duration(1)) && val.Kind() == reflect.String {
d, err := time.ParseDuration(val.String())
if err != nil {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v. %s", tval, tval, mtype.String(), err)
}
return reflect.ValueOf(d), nil
}
if !val.Type().ConvertibleTo(mtype) {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
}
if reflect.Indirect(reflect.New(mtype)).OverflowInt(val.Convert(mtype).Int()) {
return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String())
}
return val.Convert(mtype), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
val := reflect.ValueOf(tval)
if !val.Type().ConvertibleTo(mtype) {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
}
if val.Convert(reflect.TypeOf(int(1))).Int() < 0 {
return reflect.ValueOf(nil), fmt.Errorf("%v(%T) is negative so does not fit in %v", tval, tval, mtype.String())
}
if reflect.Indirect(reflect.New(mtype)).OverflowUint(uint64(val.Convert(mtype).Uint())) {
return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String())
}
return val.Convert(mtype), nil
case reflect.Float32, reflect.Float64:
val := reflect.ValueOf(tval)
if !val.Type().ConvertibleTo(mtype) {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
}
if reflect.Indirect(reflect.New(mtype)).OverflowFloat(val.Convert(mtype).Float()) {
return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String())
}
return val.Convert(mtype), nil
default:
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v(%v)", tval, tval, mtype, mtype.Kind())
}
}
}
func (d *Decoder) unwrapPointer(mtype reflect.Type, tval interface{}, mval1 *reflect.Value) (reflect.Value, error) {
var melem *reflect.Value
if mval1 != nil && !mval1.IsNil() && mtype.Elem().Kind() == reflect.Struct {
elem := mval1.Elem()
melem = &elem
}
val, err := d.valueFromToml(mtype.Elem(), tval, melem)
if err != nil {
return reflect.ValueOf(nil), err
}
mval := reflect.New(mtype.Elem())
mval.Elem().Set(val)
return mval, nil
}
func tomlOptions(vf reflect.StructField, an annotation) tomlOpts {
tag := vf.Tag.Get(an.tag)
parse := strings.Split(tag, ",")
var comment string
if c := vf.Tag.Get(an.comment); c != "" {
comment = c
}
commented, _ := strconv.ParseBool(vf.Tag.Get(an.commented))
multiline, _ := strconv.ParseBool(vf.Tag.Get(an.multiline))
defaultValue := vf.Tag.Get(tagDefault)
result := tomlOpts{
name: vf.Name,
comment: comment,
commented: commented,
multiline: multiline,
include: true,
omitempty: false,
defaultValue: defaultValue,
}
if parse[0] != "" {
if parse[0] == "-" && len(parse) == 1 {
result.include = false
} else {
result.name = strings.Trim(parse[0], " ")
}
}
if vf.PkgPath != "" {
result.include = false
}
if len(parse) > 1 && strings.Trim(parse[1], " ") == "omitempty" {
result.omitempty = true
}
if vf.Type.Kind() == reflect.Ptr {
result.omitempty = true
}
return result
}
func isZero(val reflect.Value) bool {
switch val.Type().Kind() {
case reflect.Map:
fallthrough
case reflect.Array:
fallthrough
case reflect.Slice:
return val.Len() == 0
default:
return reflect.DeepEqual(val.Interface(), reflect.Zero(val.Type()).Interface())
}
}
func formatError(err error, pos Position) error {
if err.Error()[0] == '(' { // Error already contains position information
return err
}
return fmt.Errorf("%s: %s", pos, err)
}
-39
View File
@@ -1,39 +0,0 @@
title = "TOML Marshal Testing"
[basic_lists]
floats = [12.3,45.6,78.9]
bools = [true,false,true]
dates = [1979-05-27T07:32:00Z,1980-05-27T07:32:00Z]
ints = [8001,8001,8002]
uints = [5002,5003]
strings = ["One","Two","Three"]
[[subdocptrs]]
name = "Second"
[basic_map]
one = "one"
two = "two"
[subdoc]
[subdoc.second]
name = "Second"
[subdoc.first]
name = "First"
[basic]
uint = 5001
bool = true
float = 123.4
float64 = 123.456782132399
int = 5000
string = "Bite me"
date = 1979-05-27T07:32:00Z
[[subdoclist]]
name = "List.First"
[[subdoclist]]
name = "List.Second"
-2142
View File
File diff suppressed because it is too large Load Diff
-39
View File
@@ -1,39 +0,0 @@
title = "TOML Marshal Testing"
[basic]
bool = true
date = 1979-05-27T07:32:00Z
float = 123.4
float64 = 123.456782132399
int = 5000
string = "Bite me"
uint = 5001
[basic_lists]
bools = [true,false,true]
dates = [1979-05-27T07:32:00Z,1980-05-27T07:32:00Z]
floats = [12.3,45.6,78.9]
ints = [8001,8001,8002]
strings = ["One","Two","Three"]
uints = [5002,5003]
[basic_map]
one = "one"
two = "two"
[subdoc]
[subdoc.first]
name = "First"
[subdoc.second]
name = "Second"
[[subdoclist]]
name = "List.First"
[[subdoclist]]
name = "List.Second"
[[subdocptrs]]
name = "Second"
+797
View File
@@ -0,0 +1,797 @@
package toml
import (
"bytes"
"encoding"
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
"time"
)
// Marshal serializes a Go value as a TOML document.
//
// It is a shortcut for Encoder.Encode() with the default options.
func Marshal(v interface{}) ([]byte, error) {
var buf bytes.Buffer
enc := NewEncoder(&buf)
err := enc.Encode(v)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Encoder writes a TOML document to an output stream.
type Encoder struct {
// output
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 {
// Current top-level key.
parentKey []string
// Key that should be used for a KV.
key string
// Extra flag to account for the empty string
hasKey bool
// Set to true to indicate that the encoder is inside a KV, so that all
// tables need to be inlined.
insideKv bool
// Set to true to skip the first table header in an array table.
skipTableHeader bool
// Should the next table be encoded as inline
inline bool
// Indentation level
indent int
// Options coming from struct tags
options valueOptions
}
func (ctx *encoderCtx) shiftKey() {
if ctx.hasKey {
ctx.parentKey = append(ctx.parentKey, ctx.key)
ctx.clearKey()
}
}
func (ctx *encoderCtx) setKey(k string) {
ctx.key = k
ctx.hasKey = true
}
func (ctx *encoderCtx) clearKey() {
ctx.key = ""
ctx.hasKey = false
}
func (ctx *encoderCtx) isRoot() bool {
return len(ctx.parentKey) == 0 && !ctx.hasKey
}
//nolint:cyclop,funlen
func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if !v.IsZero() {
i, ok := v.Interface().(time.Time)
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
}
switch v.Kind() {
// containers
case reflect.Map:
return enc.encodeMap(b, ctx, v)
case reflect.Struct:
return enc.encodeStruct(b, ctx, v)
case reflect.Slice:
return enc.encodeSlice(b, ctx, v)
case reflect.Interface:
if v.IsNil() {
return nil, fmt.Errorf("toml: encoding a nil interface is not supported")
}
return enc.encode(b, ctx, v.Elem())
case reflect.Ptr:
if v.IsNil() {
return enc.encode(b, ctx, reflect.Zero(v.Type().Elem()))
}
return enc.encode(b, ctx, v.Elem())
// values
case reflect.String:
b = enc.encodeString(b, v.String(), ctx.options)
case reflect.Float32:
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 32)
case reflect.Float64:
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 64)
case reflect.Bool:
if v.Bool() {
b = append(b, "true"...)
} else {
b = append(b, "false"...)
}
case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint:
b = strconv.AppendUint(b, v.Uint(), 10)
case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
b = strconv.AppendInt(b, v.Int(), 10)
default:
return nil, fmt.Errorf("toml: cannot encode value of type %s", v.Kind())
}
return b, nil
}
func isNil(v reflect.Value) bool {
switch v.Kind() {
case reflect.Ptr, reflect.Interface, reflect.Map:
return v.IsNil()
default:
return false
}
}
func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) {
var err error
if !ctx.hasKey {
panic("caller of encodeKv should have set the key in the context")
}
b = enc.indent(ctx.indent, b)
b, err = enc.encodeKey(b, ctx.key)
if err != nil {
return nil, err
}
b = append(b, " = "...)
// create a copy of the context because the value of a KV shouldn't
// modify the global context.
subctx := ctx
subctx.insideKv = true
subctx.shiftKey()
subctx.options = options
b, err = enc.encode(b, subctx, v)
if err != nil {
return nil, err
}
return b, nil
}
const literalQuote = '\''
func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byte {
if needsQuoting(v) {
return enc.encodeQuotedString(options.multiline, b, v)
}
return enc.encodeLiteralString(b, v)
}
func needsQuoting(v string) bool {
return strings.ContainsAny(v, "'\b\f\n\r\t")
}
// caller should have checked that the string does not contain new lines or ' .
func (enc *Encoder) encodeLiteralString(b []byte, v string) []byte {
b = append(b, literalQuote)
b = append(b, v...)
b = append(b, literalQuote)
return b
}
//nolint:cyclop
func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byte {
stringQuote := `"`
if multiline {
stringQuote = `"""`
}
b = append(b, stringQuote...)
if multiline {
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) {
switch r {
case '\\':
b = append(b, `\\`...)
case '"':
b = append(b, `\"`...)
case '\b':
b = append(b, `\b`...)
case '\f':
b = append(b, `\f`...)
case '\n':
if multiline {
b = append(b, r)
} else {
b = append(b, `\n`...)
}
case '\r':
b = append(b, `\r`...)
case '\t':
b = append(b, `\t`...)
default:
switch {
case r >= nul && r <= bs, r >= lf && r <= us, r == del:
b = append(b, `\u00`...)
b = append(b, hextable[r>>4])
b = append(b, hextable[r&0x0f])
default:
b = append(b, r)
}
}
}
b = append(b, stringQuote...)
return b
}
// called should have checked that the string is in A-Z / a-z / 0-9 / - / _ .
func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte {
return append(b, v...)
}
func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error) {
if len(ctx.parentKey) == 0 {
return b, nil
}
b = enc.indent(ctx.indent, b)
b = append(b, '[')
var err error
b, err = enc.encodeKey(b, ctx.parentKey[0])
if err != nil {
return nil, err
}
for _, k := range ctx.parentKey[1:] {
b = append(b, '.')
b, err = enc.encodeKey(b, k)
if err != nil {
return nil, err
}
}
b = append(b, "]\n"...)
return b, nil
}
//nolint:cyclop
func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) {
needsQuotation := false
cannotUseLiteral := false
for _, c := range k {
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
continue
}
if c == '\n' {
return nil, fmt.Errorf("toml: new line characters in keys are not supported")
}
if c == literalQuote {
cannotUseLiteral = true
}
needsQuotation = true
}
switch {
case cannotUseLiteral:
return enc.encodeQuotedString(false, b, k), nil
case needsQuotation:
return enc.encodeLiteralString(b, k), nil
default:
return enc.encodeUnquotedKey(b, k), nil
}
}
func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if v.Type().Key().Kind() != reflect.String {
return nil, fmt.Errorf("toml: type %s is not supported as a map key", v.Type().Key().Kind())
}
var (
t table
emptyValueOptions valueOptions
)
iter := v.MapRange()
for iter.Next() {
k := iter.Key().String()
v := iter.Value()
if isNil(v) {
continue
}
if willConvertToTableOrArrayTable(ctx, v) {
t.pushTable(k, v, emptyValueOptions)
} else {
t.pushKV(k, v, emptyValueOptions)
}
}
sortEntriesByKey(t.kvs)
sortEntriesByKey(t.tables)
return enc.encodeTable(b, ctx, t)
}
func sortEntriesByKey(e []entry) {
sort.Slice(e, func(i, j int) bool {
return e[i].Key < e[j].Key
})
}
type entry struct {
Key string
Value reflect.Value
Options valueOptions
}
type table struct {
kvs []entry
tables []entry
}
func (t *table) pushKV(k string, v reflect.Value, options valueOptions) {
t.kvs = append(t.kvs, entry{Key: k, Value: v, Options: options})
}
func (t *table) pushTable(k string, v reflect.Value, options valueOptions) {
t.tables = append(t.tables, entry{Key: k, Value: v, Options: options})
}
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
var t table
//nolint:godox
// TODO: cache this?
typ := v.Type()
for i := 0; i < typ.NumField(); i++ {
fieldType := typ.Field(i)
// only consider exported fields
if fieldType.PkgPath != "" {
continue
}
k, ok := fieldType.Tag.Lookup("toml")
if !ok {
k = fieldType.Name
}
// special field name to skip field
if k == "-" {
continue
}
f := v.Field(i)
if isNil(f) {
continue
}
options := valueOptions{
multiline: fieldBoolTag(fieldType, "multiline"),
}
inline := fieldBoolTag(fieldType, "inline")
if inline || !willConvertToTableOrArrayTable(ctx, f) {
t.pushKV(k, f, options)
} else {
t.pushTable(k, f, options)
}
}
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) {
var err error
ctx.shiftKey()
if ctx.insideKv || (ctx.inline && !ctx.isRoot()) {
return enc.encodeTableInline(b, ctx, t)
}
if !ctx.skipTableHeader {
b, err = enc.encodeTableHeader(ctx, b)
if err != nil {
return nil, err
}
if enc.indentTables && len(ctx.parentKey) > 0 {
ctx.indent++
}
}
ctx.skipTableHeader = false
for _, kv := range t.kvs {
ctx.setKey(kv.Key)
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
if err != nil {
return nil, err
}
b = append(b, '\n')
}
for _, table := range t.tables {
ctx.setKey(table.Key)
ctx.options = table.Options
b, err = enc.encode(b, ctx, table.Value)
if err != nil {
return nil, err
}
b = append(b, '\n')
}
return b, nil
}
func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte, error) {
var err error
b = append(b, '{')
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
}
}
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()
switch t.Kind() {
case reflect.Map, reflect.Struct:
return !ctx.inline
case reflect.Interface:
return willConvertToTable(ctx, v.Elem())
case reflect.Ptr:
if v.IsNil() {
return false
}
return willConvertToTable(ctx, v.Elem())
default:
return false
}
}
func willConvertToTableOrArrayTable(ctx encoderCtx, v reflect.Value) bool {
t := v.Type()
if t.Kind() == reflect.Interface {
return willConvertToTableOrArrayTable(ctx, v.Elem())
}
if t.Kind() == reflect.Slice {
if v.Len() == 0 {
// An empty slice should be a kv = [].
return false
}
for i := 0; i < v.Len(); i++ {
t := willConvertToTable(ctx, v.Index(i))
if !t {
return false
}
}
return true
}
return willConvertToTable(ctx, v)
}
func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if v.Len() == 0 {
b = append(b, "[]"...)
return b, nil
}
if willConvertToTableOrArrayTable(ctx, v) {
return enc.encodeSliceAsArrayTable(b, ctx, v)
}
return enc.encodeSliceAsArray(b, ctx, v)
}
// caller should have checked that v is a slice that only contains values that
// encode into tables.
func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
ctx.shiftKey()
var err error
scratch := make([]byte, 0, 64)
scratch = append(scratch, "[["...)
for i, k := range ctx.parentKey {
if i > 0 {
scratch = append(scratch, '.')
}
scratch, err = enc.encodeKey(scratch, k)
if err != nil {
return nil, err
}
}
scratch = append(scratch, "]]\n"...)
ctx.skipTableHeader = true
for i := 0; i < v.Len(); i++ {
b = append(b, scratch...)
b, err = enc.encode(b, ctx, v.Index(i))
if err != nil {
return nil, err
}
}
return b, nil
}
func (enc *Encoder) encodeSliceAsArray(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
multiline := ctx.options.multiline || enc.arraysMultiline
separator := ", "
b = append(b, '[')
subCtx := ctx
subCtx.options = valueOptions{}
if multiline {
separator = ",\n"
b = append(b, '\n')
subCtx.indent++
}
var err error
first := true
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 {
return nil, err
}
}
if multiline {
b = append(b, '\n')
b = enc.indent(ctx.indent, b)
}
b = append(b, ']')
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
}
+808
View File
@@ -0,0 +1,808 @@
package toml_test
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
//nolint:funlen
func TestMarshal(t *testing.T) {
someInt := 42
type structInline struct {
A interface{} `inline:"true"`
}
examples := []struct {
desc string
v interface{}
expected string
err bool
}{
{
desc: "simple map and string",
v: map[string]string{
"hello": "world",
},
expected: "hello = 'world'",
},
{
desc: "map with new line in key",
v: map[string]string{
"hel\nlo": "world",
},
err: true,
},
{
desc: `map with " in key`,
v: map[string]string{
`hel"lo`: "world",
},
expected: `'hel"lo' = 'world'`,
},
{
desc: "map in map and string",
v: map[string]map[string]string{
"table": {
"hello": "world",
},
},
expected: `
[table]
hello = 'world'`,
},
{
desc: "map in map in map and string",
v: map[string]map[string]map[string]string{
"this": {
"is": {
"a": "test",
},
},
},
expected: `
[this]
[this.is]
a = 'test'`,
},
{
desc: "map in map in map and string with values",
v: map[string]interface{}{
"this": map[string]interface{}{
"is": map[string]string{
"a": "test",
},
"also": "that",
},
},
expected: `
[this]
also = 'that'
[this.is]
a = 'test'`,
},
{
desc: "simple string array",
v: map[string][]string{
"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",
v: map[string][][]string{
"array": {{"one", "two"}, {"three"}},
},
expected: `array = [['one', 'two'], ['three']]`,
},
{
desc: "mixed strings and nested string arrays",
v: map[string][]interface{}{
"array": {"a string", []string{"one", "two"}, "last"},
},
expected: `array = ['a string', ['one', 'two'], 'last']`,
},
{
desc: "array of maps",
v: map[string][]map[string]string{
"top": {
{"map1.1": "v1.1"},
{"map2.1": "v2.1"},
},
},
expected: `
[[top]]
'map1.1' = 'v1.1'
[[top]]
'map2.1' = 'v2.1'
`,
},
{
desc: "map with two keys",
v: map[string]string{
"key1": "value1",
"key2": "value2",
},
expected: `
key1 = 'value1'
key2 = 'value2'`,
},
{
desc: "simple struct",
v: struct {
A string
}{
A: "foo",
},
expected: `A = 'foo'`,
},
{
desc: "one level of structs within structs",
v: struct {
A interface{}
}{
A: struct {
K1 string
K2 string
}{
K1: "v1",
K2: "v2",
},
},
expected: `
[A]
K1 = 'v1'
K2 = 'v2'
`,
},
{
desc: "structs in array with interfaces",
v: map[string]interface{}{
"root": map[string]interface{}{
"nested": []interface{}{
map[string]interface{}{"name": "Bob"},
map[string]interface{}{"name": "Alice"},
},
},
},
expected: `
[root]
[[root.nested]]
name = 'Bob'
[[root.nested]]
name = 'Alice'
`,
},
{
desc: "string escapes",
v: map[string]interface{}{
"a": `'"\`,
},
expected: `a = "'\"\\"`,
},
{
desc: "string utf8 low",
v: map[string]interface{}{
"a": "'Ę",
},
expected: `a = "'Ę"`,
},
{
desc: "string utf8 low 2",
v: map[string]interface{}{
"a": "'\u10A85",
},
expected: "a = \"'\u10A85\"",
},
{
desc: "string utf8 low 2",
v: map[string]interface{}{
"a": "'\u10A85",
},
expected: "a = \"'\u10A85\"",
},
{
desc: "emoji",
v: map[string]interface{}{
"a": "'😀",
},
expected: "a = \"'😀\"",
},
{
desc: "control char",
v: map[string]interface{}{
"a": "'\u001A",
},
expected: `a = "'\u001A"`,
},
{
desc: "multi-line string",
v: map[string]interface{}{
"a": "hello\nworld",
},
expected: `a = "hello\nworld"`,
},
{
desc: "multi-line forced",
v: struct {
A string `multiline:"true"`
}{
A: "hello\nworld",
},
expected: `A = """
hello
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 {
e := e
t.Run(e.desc, func(t *testing.T) {
b, err := toml.Marshal(e.v)
if e.err {
require.Error(t, err)
return
}
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)
})
}
}
func equalStringsIgnoreNewlines(t *testing.T, expected string, actual string) {
t.Helper()
cutset := "\n"
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) {
data := []byte(`{"a": [ { "b": { "c": "d" } } ]}`)
var v interface{}
err := json.Unmarshal(data, &v)
require.NoError(t, err)
var buf bytes.Buffer
err = toml.NewEncoder(&buf).Encode(v)
require.NoError(t, err)
expected := `
[[a]]
[a.b]
c = 'd'
`
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']
}
+987 -440
View File
File diff suppressed because it is too large Load Diff
+327 -1058
View File
File diff suppressed because it is too large Load Diff
-29
View File
@@ -1,29 +0,0 @@
// Position support for go-toml
package toml
import (
"fmt"
)
// Position of a document element within a TOML document.
//
// Line and Col are both 1-indexed positions for the element's line number and
// column number, respectively. Values of zero or less will cause Invalid(),
// to return true.
type Position struct {
Line int // line within the document
Col int // column within the line
}
// String representation of the position.
// Displays 1-indexed line and column numbers.
func (p Position) String() string {
return fmt.Sprintf("(%d, %d)", p.Line, p.Col)
}
// Invalid returns whether or not the position is valid (i.e. with negative or
// null values)
func (p Position) Invalid() bool {
return p.Line <= 0 || p.Col <= 0
}
-29
View File
@@ -1,29 +0,0 @@
// Testing support for go-toml
package toml
import (
"testing"
)
func TestPositionString(t *testing.T) {
p := Position{123, 456}
expected := "(123, 456)"
value := p.String()
if value != expected {
t.Errorf("Expected %v, got %v instead", expected, value)
}
}
func TestInvalid(t *testing.T) {
for i, v := range []Position{
{0, 1234},
{1234, 0},
{0, 0},
} {
if !v.Invalid() {
t.Errorf("Position at %v is valid: %v", i, v)
}
}
}
-175
View File
@@ -1,175 +0,0 @@
// Package query performs JSONPath-like queries on a TOML document.
//
// The query path implementation is based loosely on the JSONPath specification:
// http://goessner.net/articles/JsonPath/.
//
// The idea behind a query path is to allow quick access to any element, or set
// of elements within TOML document, with a single expression.
//
// result, err := query.CompileAndExecute("$.foo.bar.baz", tree)
//
// This is roughly equivalent to:
//
// next := tree.Get("foo")
// if next != nil {
// next = next.Get("bar")
// if next != nil {
// next = next.Get("baz")
// }
// }
// result := next
//
// err is nil if any parsing exception occurs.
//
// If no node in the tree matches the query, result will simply contain an empty list of
// items.
//
// As illustrated above, the query path is much more efficient, especially since
// the structure of the TOML file can vary. Rather than making assumptions about
// a document's structure, a query allows the programmer to make structured
// requests into the document, and get zero or more values as a result.
//
// Query syntax
//
// The syntax of a query begins with a root token, followed by any number
// sub-expressions:
//
// $
// Root of the TOML tree. This must always come first.
// .name
// Selects child of this node, where 'name' is a TOML key
// name.
// ['name']
// Selects child of this node, where 'name' is a string
// containing a TOML key name.
// [index]
// Selcts child array element at 'index'.
// ..expr
// Recursively selects all children, filtered by an a union,
// index, or slice expression.
// ..*
// Recursive selection of all nodes at this point in the
// tree.
// .*
// Selects all children of the current node.
// [expr,expr]
// Union operator - a logical 'or' grouping of two or more
// sub-expressions: index, key name, or filter.
// [start:end:step]
// Slice operator - selects array elements from start to
// end-1, at the given step. All three arguments are
// optional.
// [?(filter)]
// Named filter expression - the function 'filter' is
// used to filter children at this node.
//
// Query Indexes And Slices
//
// Index expressions perform no bounds checking, and will contribute no
// values to the result set if the provided index or index range is invalid.
// Negative indexes represent values from the end of the array, counting backwards.
//
// // select the last index of the array named 'foo'
// query.CompileAndExecute("$.foo[-1]", tree)
//
// Slice expressions are supported, by using ':' to separate a start/end index pair.
//
// // select up to the first five elements in the array
// query.CompileAndExecute("$.foo[0:5]", tree)
//
// Slice expressions also allow negative indexes for the start and stop
// arguments.
//
// // select all array elements.
// query.CompileAndExecute("$.foo[0:-1]", tree)
//
// Slice expressions may have an optional stride/step parameter:
//
// // select every other element
// query.CompileAndExecute("$.foo[0:-1:2]", tree)
//
// Slice start and end parameters are also optional:
//
// // these are all equivalent and select all the values in the array
// query.CompileAndExecute("$.foo[:]", tree)
// query.CompileAndExecute("$.foo[0:]", tree)
// query.CompileAndExecute("$.foo[:-1]", tree)
// query.CompileAndExecute("$.foo[0:-1:]", tree)
// query.CompileAndExecute("$.foo[::1]", tree)
// query.CompileAndExecute("$.foo[0::1]", tree)
// query.CompileAndExecute("$.foo[:-1:1]", tree)
// query.CompileAndExecute("$.foo[0:-1:1]", tree)
//
// Query Filters
//
// Query filters are used within a Union [,] or single Filter [] expression.
// A filter only allows nodes that qualify through to the next expression,
// and/or into the result set.
//
// // returns children of foo that are permitted by the 'bar' filter.
// query.CompileAndExecute("$.foo[?(bar)]", tree)
//
// There are several filters provided with the library:
//
// tree
// Allows nodes of type Tree.
// int
// Allows nodes of type int64.
// float
// Allows nodes of type float64.
// string
// Allows nodes of type string.
// time
// Allows nodes of type time.Time.
// bool
// Allows nodes of type bool.
//
// Query Results
//
// An executed query returns a Result object. This contains the nodes
// in the TOML tree that qualify the query expression. Position information
// is also available for each value in the set.
//
// // display the results of a query
// results := query.CompileAndExecute("$.foo.bar.baz", tree)
// for idx, value := results.Values() {
// fmt.Println("%v: %v", results.Positions()[idx], value)
// }
//
// Compiled Queries
//
// Queries may be executed directly on a Tree object, or compiled ahead
// of time and executed discretely. The former is more convenient, but has the
// penalty of having to recompile the query expression each time.
//
// // basic query
// results := query.CompileAndExecute("$.foo.bar.baz", tree)
//
// // compiled query
// query, err := toml.Compile("$.foo.bar.baz")
// results := query.Execute(tree)
//
// // run the compiled query again on a different tree
// moreResults := query.Execute(anotherTree)
//
// User Defined Query Filters
//
// Filter expressions may also be user defined by using the SetFilter()
// function on the Query object. The function must return true/false, which
// signifies if the passed node is kept or discarded, respectively.
//
// // create a query that references a user-defined filter
// query, _ := query.Compile("$[?(bazOnly)]")
//
// // define the filter, and assign it to the query
// query.SetFilter("bazOnly", func(node interface{}) bool{
// if tree, ok := node.(*Tree); ok {
// return tree.Has("baz")
// }
// return false // reject all other node types
// })
//
// // run the query
// query.Execute(tree)
//
package query
-357
View File
@@ -1,357 +0,0 @@
// TOML JSONPath lexer.
//
// Written using the principles developed by Rob Pike in
// http://www.youtube.com/watch?v=HxaD_trXwRE
package query
import (
"fmt"
"github.com/pelletier/go-toml"
"strconv"
"strings"
"unicode/utf8"
)
// Lexer state function
type queryLexStateFn func() queryLexStateFn
// Lexer definition
type queryLexer struct {
input string
start int
pos int
width int
tokens chan token
depth int
line int
col int
stringTerm string
}
func (l *queryLexer) run() {
for state := l.lexVoid; state != nil; {
state = state()
}
close(l.tokens)
}
func (l *queryLexer) nextStart() {
// iterate by runes (utf8 characters)
// search for newlines and advance line/col counts
for i := l.start; i < l.pos; {
r, width := utf8.DecodeRuneInString(l.input[i:])
if r == '\n' {
l.line++
l.col = 1
} else {
l.col++
}
i += width
}
// advance start position to next token
l.start = l.pos
}
func (l *queryLexer) emit(t tokenType) {
l.tokens <- token{
Position: toml.Position{Line: l.line, Col: l.col},
typ: t,
val: l.input[l.start:l.pos],
}
l.nextStart()
}
func (l *queryLexer) emitWithValue(t tokenType, value string) {
l.tokens <- token{
Position: toml.Position{Line: l.line, Col: l.col},
typ: t,
val: value,
}
l.nextStart()
}
func (l *queryLexer) next() rune {
if l.pos >= len(l.input) {
l.width = 0
return eof
}
var r rune
r, l.width = utf8.DecodeRuneInString(l.input[l.pos:])
l.pos += l.width
return r
}
func (l *queryLexer) ignore() {
l.nextStart()
}
func (l *queryLexer) backup() {
l.pos -= l.width
}
func (l *queryLexer) errorf(format string, args ...interface{}) queryLexStateFn {
l.tokens <- token{
Position: toml.Position{Line: l.line, Col: l.col},
typ: tokenError,
val: fmt.Sprintf(format, args...),
}
return nil
}
func (l *queryLexer) peek() rune {
r := l.next()
l.backup()
return r
}
func (l *queryLexer) accept(valid string) bool {
if strings.ContainsRune(valid, l.next()) {
return true
}
l.backup()
return false
}
func (l *queryLexer) follow(next string) bool {
return strings.HasPrefix(l.input[l.pos:], next)
}
func (l *queryLexer) lexVoid() queryLexStateFn {
for {
next := l.peek()
switch next {
case '$':
l.pos++
l.emit(tokenDollar)
continue
case '.':
if l.follow("..") {
l.pos += 2
l.emit(tokenDotDot)
} else {
l.pos++
l.emit(tokenDot)
}
continue
case '[':
l.pos++
l.emit(tokenLeftBracket)
continue
case ']':
l.pos++
l.emit(tokenRightBracket)
continue
case ',':
l.pos++
l.emit(tokenComma)
continue
case '*':
l.pos++
l.emit(tokenStar)
continue
case '(':
l.pos++
l.emit(tokenLeftParen)
continue
case ')':
l.pos++
l.emit(tokenRightParen)
continue
case '?':
l.pos++
l.emit(tokenQuestion)
continue
case ':':
l.pos++
l.emit(tokenColon)
continue
case '\'':
l.ignore()
l.stringTerm = string(next)
return l.lexString
case '"':
l.ignore()
l.stringTerm = string(next)
return l.lexString
}
if isSpace(next) {
l.next()
l.ignore()
continue
}
if isAlphanumeric(next) {
return l.lexKey
}
if next == '+' || next == '-' || isDigit(next) {
return l.lexNumber
}
if l.next() == eof {
break
}
return l.errorf("unexpected char: '%v'", next)
}
l.emit(tokenEOF)
return nil
}
func (l *queryLexer) lexKey() queryLexStateFn {
for {
next := l.peek()
if !isAlphanumeric(next) {
l.emit(tokenKey)
return l.lexVoid
}
if l.next() == eof {
break
}
}
l.emit(tokenEOF)
return nil
}
func (l *queryLexer) lexString() queryLexStateFn {
l.pos++
l.ignore()
growingString := ""
for {
if l.follow(l.stringTerm) {
l.emitWithValue(tokenString, growingString)
l.pos++
l.ignore()
return l.lexVoid
}
if l.follow("\\\"") {
l.pos++
growingString += "\""
} else if l.follow("\\'") {
l.pos++
growingString += "'"
} else if l.follow("\\n") {
l.pos++
growingString += "\n"
} else if l.follow("\\b") {
l.pos++
growingString += "\b"
} else if l.follow("\\f") {
l.pos++
growingString += "\f"
} else if l.follow("\\/") {
l.pos++
growingString += "/"
} else if l.follow("\\t") {
l.pos++
growingString += "\t"
} else if l.follow("\\r") {
l.pos++
growingString += "\r"
} else if l.follow("\\\\") {
l.pos++
growingString += "\\"
} else if l.follow("\\u") {
l.pos += 2
code := ""
for i := 0; i < 4; i++ {
c := l.peek()
l.pos++
if !isHexDigit(c) {
return l.errorf("unfinished unicode escape")
}
code = code + string(c)
}
l.pos--
intcode, err := strconv.ParseInt(code, 16, 32)
if err != nil {
return l.errorf("invalid unicode escape: \\u" + code)
}
growingString += string(rune(intcode))
} else if l.follow("\\U") {
l.pos += 2
code := ""
for i := 0; i < 8; i++ {
c := l.peek()
l.pos++
if !isHexDigit(c) {
return l.errorf("unfinished unicode escape")
}
code = code + string(c)
}
l.pos--
intcode, err := strconv.ParseInt(code, 16, 32)
if err != nil {
return l.errorf("invalid unicode escape: \\u" + code)
}
growingString += string(rune(intcode))
} else if l.follow("\\") {
l.pos++
return l.errorf("invalid escape sequence: \\" + string(l.peek()))
} else {
growingString += string(l.peek())
}
if l.next() == eof {
break
}
}
return l.errorf("unclosed string")
}
func (l *queryLexer) lexNumber() queryLexStateFn {
l.ignore()
if !l.accept("+") {
l.accept("-")
}
pointSeen := false
digitSeen := false
for {
next := l.next()
if next == '.' {
if pointSeen {
return l.errorf("cannot have two dots in one float")
}
if !isDigit(l.peek()) {
return l.errorf("float cannot end with a dot")
}
pointSeen = true
} else if isDigit(next) {
digitSeen = true
} else {
l.backup()
break
}
if pointSeen && !digitSeen {
return l.errorf("cannot start float with a dot")
}
}
if !digitSeen {
return l.errorf("no digit in that number")
}
if pointSeen {
l.emit(tokenFloat)
} else {
l.emit(tokenInteger)
}
return l.lexVoid
}
// Entry point
func lexQuery(input string) chan token {
l := &queryLexer{
input: input,
tokens: make(chan token),
line: 1,
col: 1,
}
go l.run()
return l.tokens
}
-179
View File
@@ -1,179 +0,0 @@
package query
import (
"github.com/pelletier/go-toml"
"testing"
)
func testQLFlow(t *testing.T, input string, expectedFlow []token) {
ch := lexQuery(input)
for idx, expected := range expectedFlow {
token := <-ch
if token != expected {
t.Log("While testing #", idx, ":", input)
t.Log("compared (got)", token, "to (expected)", expected)
t.Log("\tvalue:", token.val, "<->", expected.val)
t.Log("\tvalue as bytes:", []byte(token.val), "<->", []byte(expected.val))
t.Log("\ttype:", token.typ.String(), "<->", expected.typ.String())
t.Log("\tline:", token.Line, "<->", expected.Line)
t.Log("\tcolumn:", token.Col, "<->", expected.Col)
t.Log("compared", token, "to", expected)
t.FailNow()
}
}
tok, ok := <-ch
if ok {
t.Log("channel is not closed!")
t.Log(len(ch)+1, "tokens remaining:")
t.Log("token ->", tok)
for token := range ch {
t.Log("token ->", token)
}
t.FailNow()
}
}
func TestLexSpecialChars(t *testing.T) {
testQLFlow(t, " .$[]..()?*", []token{
{toml.Position{1, 2}, tokenDot, "."},
{toml.Position{1, 3}, tokenDollar, "$"},
{toml.Position{1, 4}, tokenLeftBracket, "["},
{toml.Position{1, 5}, tokenRightBracket, "]"},
{toml.Position{1, 6}, tokenDotDot, ".."},
{toml.Position{1, 8}, tokenLeftParen, "("},
{toml.Position{1, 9}, tokenRightParen, ")"},
{toml.Position{1, 10}, tokenQuestion, "?"},
{toml.Position{1, 11}, tokenStar, "*"},
{toml.Position{1, 12}, tokenEOF, ""},
})
}
func TestLexString(t *testing.T) {
testQLFlow(t, "'foo\n'", []token{
{toml.Position{1, 2}, tokenString, "foo\n"},
{toml.Position{2, 2}, tokenEOF, ""},
})
}
func TestLexDoubleString(t *testing.T) {
testQLFlow(t, `"bar"`, []token{
{toml.Position{1, 2}, tokenString, "bar"},
{toml.Position{1, 6}, tokenEOF, ""},
})
}
func TestLexStringEscapes(t *testing.T) {
testQLFlow(t, `"foo \" \' \b \f \/ \t \r \\ \u03A9 \U00012345 \n bar"`, []token{
{toml.Position{1, 2}, tokenString, "foo \" ' \b \f / \t \r \\ \u03A9 \U00012345 \n bar"},
{toml.Position{1, 55}, tokenEOF, ""},
})
}
func TestLexStringUnfinishedUnicode4(t *testing.T) {
testQLFlow(t, `"\u000"`, []token{
{toml.Position{1, 2}, tokenError, "unfinished unicode escape"},
})
}
func TestLexStringUnfinishedUnicode8(t *testing.T) {
testQLFlow(t, `"\U0000"`, []token{
{toml.Position{1, 2}, tokenError, "unfinished unicode escape"},
})
}
func TestLexStringInvalidEscape(t *testing.T) {
testQLFlow(t, `"\x"`, []token{
{toml.Position{1, 2}, tokenError, "invalid escape sequence: \\x"},
})
}
func TestLexStringUnfinished(t *testing.T) {
testQLFlow(t, `"bar`, []token{
{toml.Position{1, 2}, tokenError, "unclosed string"},
})
}
func TestLexKey(t *testing.T) {
testQLFlow(t, "foo", []token{
{toml.Position{1, 1}, tokenKey, "foo"},
{toml.Position{1, 4}, tokenEOF, ""},
})
}
func TestLexRecurse(t *testing.T) {
testQLFlow(t, "$..*", []token{
{toml.Position{1, 1}, tokenDollar, "$"},
{toml.Position{1, 2}, tokenDotDot, ".."},
{toml.Position{1, 4}, tokenStar, "*"},
{toml.Position{1, 5}, tokenEOF, ""},
})
}
func TestLexBracketKey(t *testing.T) {
testQLFlow(t, "$[foo]", []token{
{toml.Position{1, 1}, tokenDollar, "$"},
{toml.Position{1, 2}, tokenLeftBracket, "["},
{toml.Position{1, 3}, tokenKey, "foo"},
{toml.Position{1, 6}, tokenRightBracket, "]"},
{toml.Position{1, 7}, tokenEOF, ""},
})
}
func TestLexSpace(t *testing.T) {
testQLFlow(t, "foo bar baz", []token{
{toml.Position{1, 1}, tokenKey, "foo"},
{toml.Position{1, 5}, tokenKey, "bar"},
{toml.Position{1, 9}, tokenKey, "baz"},
{toml.Position{1, 12}, tokenEOF, ""},
})
}
func TestLexInteger(t *testing.T) {
testQLFlow(t, "100 +200 -300", []token{
{toml.Position{1, 1}, tokenInteger, "100"},
{toml.Position{1, 5}, tokenInteger, "+200"},
{toml.Position{1, 10}, tokenInteger, "-300"},
{toml.Position{1, 14}, tokenEOF, ""},
})
}
func TestLexFloat(t *testing.T) {
testQLFlow(t, "100.0 +200.0 -300.0", []token{
{toml.Position{1, 1}, tokenFloat, "100.0"},
{toml.Position{1, 7}, tokenFloat, "+200.0"},
{toml.Position{1, 14}, tokenFloat, "-300.0"},
{toml.Position{1, 20}, tokenEOF, ""},
})
}
func TestLexFloatWithMultipleDots(t *testing.T) {
testQLFlow(t, "4.2.", []token{
{toml.Position{1, 1}, tokenError, "cannot have two dots in one float"},
})
}
func TestLexFloatLeadingDot(t *testing.T) {
testQLFlow(t, "+.1", []token{
{toml.Position{1, 1}, tokenError, "cannot start float with a dot"},
})
}
func TestLexFloatWithTrailingDot(t *testing.T) {
testQLFlow(t, "42.", []token{
{toml.Position{1, 1}, tokenError, "float cannot end with a dot"},
})
}
func TestLexNumberWithoutDigit(t *testing.T) {
testQLFlow(t, "+", []token{
{toml.Position{1, 1}, tokenError, "no digit in that number"},
})
}
func TestLexUnknown(t *testing.T) {
testQLFlow(t, "^", []token{
{toml.Position{1, 1}, tokenError, "unexpected char: '94'"},
})
}
-232
View File
@@ -1,232 +0,0 @@
package query
import (
"fmt"
"github.com/pelletier/go-toml"
)
// base match
type matchBase struct {
next pathFn
}
func (f *matchBase) setNext(next pathFn) {
f.next = next
}
// terminating functor - gathers results
type terminatingFn struct {
// empty
}
func newTerminatingFn() *terminatingFn {
return &terminatingFn{}
}
func (f *terminatingFn) setNext(next pathFn) {
// do nothing
}
func (f *terminatingFn) call(node interface{}, ctx *queryContext) {
ctx.result.appendResult(node, ctx.lastPosition)
}
// match single key
type matchKeyFn struct {
matchBase
Name string
}
func newMatchKeyFn(name string) *matchKeyFn {
return &matchKeyFn{Name: name}
}
func (f *matchKeyFn) call(node interface{}, ctx *queryContext) {
if array, ok := node.([]*toml.Tree); ok {
for _, tree := range array {
item := tree.Get(f.Name)
if item != nil {
ctx.lastPosition = tree.GetPosition(f.Name)
f.next.call(item, ctx)
}
}
} else if tree, ok := node.(*toml.Tree); ok {
item := tree.Get(f.Name)
if item != nil {
ctx.lastPosition = tree.GetPosition(f.Name)
f.next.call(item, ctx)
}
}
}
// match single index
type matchIndexFn struct {
matchBase
Idx int
}
func newMatchIndexFn(idx int) *matchIndexFn {
return &matchIndexFn{Idx: idx}
}
func (f *matchIndexFn) call(node interface{}, ctx *queryContext) {
if arr, ok := node.([]interface{}); ok {
if f.Idx < len(arr) && f.Idx >= 0 {
if treesArray, ok := node.([]*toml.Tree); ok {
if len(treesArray) > 0 {
ctx.lastPosition = treesArray[0].Position()
}
}
f.next.call(arr[f.Idx], ctx)
}
}
}
// filter by slicing
type matchSliceFn struct {
matchBase
Start, End, Step int
}
func newMatchSliceFn(start, end, step int) *matchSliceFn {
return &matchSliceFn{Start: start, End: end, Step: step}
}
func (f *matchSliceFn) call(node interface{}, ctx *queryContext) {
if arr, ok := node.([]interface{}); ok {
// adjust indexes for negative values, reverse ordering
realStart, realEnd := f.Start, f.End
if realStart < 0 {
realStart = len(arr) + realStart
}
if realEnd < 0 {
realEnd = len(arr) + realEnd
}
if realEnd < realStart {
realEnd, realStart = realStart, realEnd // swap
}
// loop and gather
for idx := realStart; idx < realEnd; idx += f.Step {
if treesArray, ok := node.([]*toml.Tree); ok {
if len(treesArray) > 0 {
ctx.lastPosition = treesArray[0].Position()
}
}
f.next.call(arr[idx], ctx)
}
}
}
// match anything
type matchAnyFn struct {
matchBase
}
func newMatchAnyFn() *matchAnyFn {
return &matchAnyFn{}
}
func (f *matchAnyFn) call(node interface{}, ctx *queryContext) {
if tree, ok := node.(*toml.Tree); ok {
for _, k := range tree.Keys() {
v := tree.Get(k)
ctx.lastPosition = tree.GetPosition(k)
f.next.call(v, ctx)
}
}
}
// filter through union
type matchUnionFn struct {
Union []pathFn
}
func (f *matchUnionFn) setNext(next pathFn) {
for _, fn := range f.Union {
fn.setNext(next)
}
}
func (f *matchUnionFn) call(node interface{}, ctx *queryContext) {
for _, fn := range f.Union {
fn.call(node, ctx)
}
}
// match every single last node in the tree
type matchRecursiveFn struct {
matchBase
}
func newMatchRecursiveFn() *matchRecursiveFn {
return &matchRecursiveFn{}
}
func (f *matchRecursiveFn) call(node interface{}, ctx *queryContext) {
originalPosition := ctx.lastPosition
if tree, ok := node.(*toml.Tree); ok {
var visit func(tree *toml.Tree)
visit = func(tree *toml.Tree) {
for _, k := range tree.Keys() {
v := tree.Get(k)
ctx.lastPosition = tree.GetPosition(k)
f.next.call(v, ctx)
switch node := v.(type) {
case *toml.Tree:
visit(node)
case []*toml.Tree:
for _, subtree := range node {
visit(subtree)
}
}
}
}
ctx.lastPosition = originalPosition
f.next.call(tree, ctx)
visit(tree)
}
}
// match based on an externally provided functional filter
type matchFilterFn struct {
matchBase
Pos toml.Position
Name string
}
func newMatchFilterFn(name string, pos toml.Position) *matchFilterFn {
return &matchFilterFn{Name: name, Pos: pos}
}
func (f *matchFilterFn) call(node interface{}, ctx *queryContext) {
fn, ok := (*ctx.filters)[f.Name]
if !ok {
panic(fmt.Sprintf("%s: query context does not have filter '%s'",
f.Pos.String(), f.Name))
}
switch castNode := node.(type) {
case *toml.Tree:
for _, k := range castNode.Keys() {
v := castNode.Get(k)
if fn(v) {
ctx.lastPosition = castNode.GetPosition(k)
f.next.call(v, ctx)
}
}
case []*toml.Tree:
for _, v := range castNode {
if fn(v) {
if len(castNode) > 0 {
ctx.lastPosition = castNode[0].Position()
}
f.next.call(v, ctx)
}
}
case []interface{}:
for _, v := range castNode {
if fn(v) {
f.next.call(v, ctx)
}
}
}
}
-202
View File
@@ -1,202 +0,0 @@
package query
import (
"fmt"
"github.com/pelletier/go-toml"
"testing"
)
// dump path tree to a string
func pathString(root pathFn) string {
result := fmt.Sprintf("%T:", root)
switch fn := root.(type) {
case *terminatingFn:
result += "{}"
case *matchKeyFn:
result += fmt.Sprintf("{%s}", fn.Name)
result += pathString(fn.next)
case *matchIndexFn:
result += fmt.Sprintf("{%d}", fn.Idx)
result += pathString(fn.next)
case *matchSliceFn:
result += fmt.Sprintf("{%d:%d:%d}",
fn.Start, fn.End, fn.Step)
result += pathString(fn.next)
case *matchAnyFn:
result += "{}"
result += pathString(fn.next)
case *matchUnionFn:
result += "{["
for _, v := range fn.Union {
result += pathString(v) + ", "
}
result += "]}"
case *matchRecursiveFn:
result += "{}"
result += pathString(fn.next)
case *matchFilterFn:
result += fmt.Sprintf("{%s}", fn.Name)
result += pathString(fn.next)
}
return result
}
func assertPathMatch(t *testing.T, path, ref *Query) bool {
pathStr := pathString(path.root)
refStr := pathString(ref.root)
if pathStr != refStr {
t.Errorf("paths do not match")
t.Log("test:", pathStr)
t.Log("ref: ", refStr)
return false
}
return true
}
func assertPath(t *testing.T, query string, ref *Query) {
path, _ := parseQuery(lexQuery(query))
assertPathMatch(t, path, ref)
}
func buildPath(parts ...pathFn) *Query {
query := newQuery()
for _, v := range parts {
query.appendPath(v)
}
return query
}
func TestPathRoot(t *testing.T) {
assertPath(t,
"$",
buildPath(
// empty
))
}
func TestPathKey(t *testing.T) {
assertPath(t,
"$.foo",
buildPath(
newMatchKeyFn("foo"),
))
}
func TestPathBracketKey(t *testing.T) {
assertPath(t,
"$[foo]",
buildPath(
newMatchKeyFn("foo"),
))
}
func TestPathBracketStringKey(t *testing.T) {
assertPath(t,
"$['foo']",
buildPath(
newMatchKeyFn("foo"),
))
}
func TestPathIndex(t *testing.T) {
assertPath(t,
"$[123]",
buildPath(
newMatchIndexFn(123),
))
}
func TestPathSliceStart(t *testing.T) {
assertPath(t,
"$[123:]",
buildPath(
newMatchSliceFn(123, maxInt, 1),
))
}
func TestPathSliceStartEnd(t *testing.T) {
assertPath(t,
"$[123:456]",
buildPath(
newMatchSliceFn(123, 456, 1),
))
}
func TestPathSliceStartEndColon(t *testing.T) {
assertPath(t,
"$[123:456:]",
buildPath(
newMatchSliceFn(123, 456, 1),
))
}
func TestPathSliceStartStep(t *testing.T) {
assertPath(t,
"$[123::7]",
buildPath(
newMatchSliceFn(123, maxInt, 7),
))
}
func TestPathSliceEndStep(t *testing.T) {
assertPath(t,
"$[:456:7]",
buildPath(
newMatchSliceFn(0, 456, 7),
))
}
func TestPathSliceStep(t *testing.T) {
assertPath(t,
"$[::7]",
buildPath(
newMatchSliceFn(0, maxInt, 7),
))
}
func TestPathSliceAll(t *testing.T) {
assertPath(t,
"$[123:456:7]",
buildPath(
newMatchSliceFn(123, 456, 7),
))
}
func TestPathAny(t *testing.T) {
assertPath(t,
"$.*",
buildPath(
newMatchAnyFn(),
))
}
func TestPathUnion(t *testing.T) {
assertPath(t,
"$[foo, bar, baz]",
buildPath(
&matchUnionFn{[]pathFn{
newMatchKeyFn("foo"),
newMatchKeyFn("bar"),
newMatchKeyFn("baz"),
}},
))
}
func TestPathRecurse(t *testing.T) {
assertPath(t,
"$..*",
buildPath(
newMatchRecursiveFn(),
))
}
func TestPathFilterExpr(t *testing.T) {
assertPath(t,
"$[?('foo'),?(bar)]",
buildPath(
&matchUnionFn{[]pathFn{
newMatchFilterFn("foo", toml.Position{}),
newMatchFilterFn("bar", toml.Position{}),
}},
))
}
-275
View File
@@ -1,275 +0,0 @@
/*
Based on the "jsonpath" spec/concept.
http://goessner.net/articles/JsonPath/
https://code.google.com/p/json-path/
*/
package query
import (
"fmt"
)
const maxInt = int(^uint(0) >> 1)
type queryParser struct {
flow chan token
tokensBuffer []token
query *Query
union []pathFn
err error
}
type queryParserStateFn func() queryParserStateFn
// Formats and panics an error message based on a token
func (p *queryParser) parseError(tok *token, msg string, args ...interface{}) queryParserStateFn {
p.err = fmt.Errorf(tok.Position.String()+": "+msg, args...)
return nil // trigger parse to end
}
func (p *queryParser) run() {
for state := p.parseStart; state != nil; {
state = state()
}
}
func (p *queryParser) backup(tok *token) {
p.tokensBuffer = append(p.tokensBuffer, *tok)
}
func (p *queryParser) peek() *token {
if len(p.tokensBuffer) != 0 {
return &(p.tokensBuffer[0])
}
tok, ok := <-p.flow
if !ok {
return nil
}
p.backup(&tok)
return &tok
}
func (p *queryParser) lookahead(types ...tokenType) bool {
result := true
buffer := []token{}
for _, typ := range types {
tok := p.getToken()
if tok == nil {
result = false
break
}
buffer = append(buffer, *tok)
if tok.typ != typ {
result = false
break
}
}
// add the tokens back to the buffer, and return
p.tokensBuffer = append(p.tokensBuffer, buffer...)
return result
}
func (p *queryParser) getToken() *token {
if len(p.tokensBuffer) != 0 {
tok := p.tokensBuffer[0]
p.tokensBuffer = p.tokensBuffer[1:]
return &tok
}
tok, ok := <-p.flow
if !ok {
return nil
}
return &tok
}
func (p *queryParser) parseStart() queryParserStateFn {
tok := p.getToken()
if tok == nil || tok.typ == tokenEOF {
return nil
}
if tok.typ != tokenDollar {
return p.parseError(tok, "Expected '$' at start of expression")
}
return p.parseMatchExpr
}
// handle '.' prefix, '[]', and '..'
func (p *queryParser) parseMatchExpr() queryParserStateFn {
tok := p.getToken()
switch tok.typ {
case tokenDotDot:
p.query.appendPath(&matchRecursiveFn{})
// nested parse for '..'
tok := p.getToken()
switch tok.typ {
case tokenKey:
p.query.appendPath(newMatchKeyFn(tok.val))
return p.parseMatchExpr
case tokenLeftBracket:
return p.parseBracketExpr
case tokenStar:
// do nothing - the recursive predicate is enough
return p.parseMatchExpr
}
case tokenDot:
// nested parse for '.'
tok := p.getToken()
switch tok.typ {
case tokenKey:
p.query.appendPath(newMatchKeyFn(tok.val))
return p.parseMatchExpr
case tokenStar:
p.query.appendPath(&matchAnyFn{})
return p.parseMatchExpr
}
case tokenLeftBracket:
return p.parseBracketExpr
case tokenEOF:
return nil // allow EOF at this stage
}
return p.parseError(tok, "expected match expression")
}
func (p *queryParser) parseBracketExpr() queryParserStateFn {
if p.lookahead(tokenInteger, tokenColon) {
return p.parseSliceExpr
}
if p.peek().typ == tokenColon {
return p.parseSliceExpr
}
return p.parseUnionExpr
}
func (p *queryParser) parseUnionExpr() queryParserStateFn {
var tok *token
// this state can be traversed after some sub-expressions
// so be careful when setting up state in the parser
if p.union == nil {
p.union = []pathFn{}
}
loop: // labeled loop for easy breaking
for {
if len(p.union) > 0 {
// parse delimiter or terminator
tok = p.getToken()
switch tok.typ {
case tokenComma:
// do nothing
case tokenRightBracket:
break loop
default:
return p.parseError(tok, "expected ',' or ']', not '%s'", tok.val)
}
}
// parse sub expression
tok = p.getToken()
switch tok.typ {
case tokenInteger:
p.union = append(p.union, newMatchIndexFn(tok.Int()))
case tokenKey:
p.union = append(p.union, newMatchKeyFn(tok.val))
case tokenString:
p.union = append(p.union, newMatchKeyFn(tok.val))
case tokenQuestion:
return p.parseFilterExpr
default:
return p.parseError(tok, "expected union sub expression, not '%s', %d", tok.val, len(p.union))
}
}
// if there is only one sub-expression, use that instead
if len(p.union) == 1 {
p.query.appendPath(p.union[0])
} else {
p.query.appendPath(&matchUnionFn{p.union})
}
p.union = nil // clear out state
return p.parseMatchExpr
}
func (p *queryParser) parseSliceExpr() queryParserStateFn {
// init slice to grab all elements
start, end, step := 0, maxInt, 1
// parse optional start
tok := p.getToken()
if tok.typ == tokenInteger {
start = tok.Int()
tok = p.getToken()
}
if tok.typ != tokenColon {
return p.parseError(tok, "expected ':'")
}
// parse optional end
tok = p.getToken()
if tok.typ == tokenInteger {
end = tok.Int()
tok = p.getToken()
}
if tok.typ == tokenRightBracket {
p.query.appendPath(newMatchSliceFn(start, end, step))
return p.parseMatchExpr
}
if tok.typ != tokenColon {
return p.parseError(tok, "expected ']' or ':'")
}
// parse optional step
tok = p.getToken()
if tok.typ == tokenInteger {
step = tok.Int()
if step < 0 {
return p.parseError(tok, "step must be a positive value")
}
tok = p.getToken()
}
if tok.typ != tokenRightBracket {
return p.parseError(tok, "expected ']'")
}
p.query.appendPath(newMatchSliceFn(start, end, step))
return p.parseMatchExpr
}
func (p *queryParser) parseFilterExpr() queryParserStateFn {
tok := p.getToken()
if tok.typ != tokenLeftParen {
return p.parseError(tok, "expected left-parenthesis for filter expression")
}
tok = p.getToken()
if tok.typ != tokenKey && tok.typ != tokenString {
return p.parseError(tok, "expected key or string for filter function name")
}
name := tok.val
tok = p.getToken()
if tok.typ != tokenRightParen {
return p.parseError(tok, "expected right-parenthesis for filter expression")
}
p.union = append(p.union, newMatchFilterFn(name, tok.Position))
return p.parseUnionExpr
}
func parseQuery(flow chan token) (*Query, error) {
parser := &queryParser{
flow: flow,
tokensBuffer: []token{},
query: newQuery(),
}
parser.run()
return parser.query, parser.err
}
-482
View File
@@ -1,482 +0,0 @@
package query
import (
"fmt"
"io/ioutil"
"sort"
"strings"
"testing"
"time"
"github.com/pelletier/go-toml"
)
type queryTestNode struct {
value interface{}
position toml.Position
}
func valueString(root interface{}) string {
result := "" //fmt.Sprintf("%T:", root)
switch node := root.(type) {
case *Result:
items := []string{}
for i, v := range node.Values() {
items = append(items, fmt.Sprintf("%s:%s",
node.Positions()[i].String(), valueString(v)))
}
sort.Strings(items)
result = "[" + strings.Join(items, ", ") + "]"
case queryTestNode:
result = fmt.Sprintf("%s:%s",
node.position.String(), valueString(node.value))
case []interface{}:
items := []string{}
for _, v := range node {
items = append(items, valueString(v))
}
sort.Strings(items)
result = "[" + strings.Join(items, ", ") + "]"
case *toml.Tree:
// workaround for unreliable map key ordering
items := []string{}
for _, k := range node.Keys() {
v := node.GetPath([]string{k})
items = append(items, k+":"+valueString(v))
}
sort.Strings(items)
result = "{" + strings.Join(items, ", ") + "}"
case map[string]interface{}:
// workaround for unreliable map key ordering
items := []string{}
for k, v := range node {
items = append(items, k+":"+valueString(v))
}
sort.Strings(items)
result = "{" + strings.Join(items, ", ") + "}"
case int64:
result += fmt.Sprintf("%d", node)
case string:
result += "'" + node + "'"
case float64:
result += fmt.Sprintf("%f", node)
case bool:
result += fmt.Sprintf("%t", node)
case time.Time:
result += fmt.Sprintf("'%v'", node)
}
return result
}
func assertValue(t *testing.T, result, ref interface{}) {
pathStr := valueString(result)
refStr := valueString(ref)
if pathStr != refStr {
t.Errorf("values do not match")
t.Log("test:", pathStr)
t.Log("ref: ", refStr)
}
}
func assertQueryPositions(t *testing.T, tomlDoc string, query string, ref []interface{}) {
tree, err := toml.Load(tomlDoc)
if err != nil {
t.Errorf("Non-nil toml parse error: %v", err)
return
}
q, err := Compile(query)
if err != nil {
t.Error(err)
return
}
results := q.Execute(tree)
assertValue(t, results, ref)
}
func TestQueryRoot(t *testing.T) {
assertQueryPositions(t,
"a = 42",
"$",
[]interface{}{
queryTestNode{
map[string]interface{}{
"a": int64(42),
}, toml.Position{1, 1},
},
})
}
func TestQueryKey(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = 42",
"$.foo.a",
[]interface{}{
queryTestNode{
int64(42), toml.Position{2, 1},
},
})
}
func TestQueryKeyString(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = 42",
"$.foo['a']",
[]interface{}{
queryTestNode{
int64(42), toml.Position{2, 1},
},
})
}
func TestQueryIndex(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [1,2,3,4,5,6,7,8,9,0]",
"$.foo.a[5]",
[]interface{}{
queryTestNode{
int64(6), toml.Position{2, 1},
},
})
}
func TestQuerySliceRange(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [1,2,3,4,5,6,7,8,9,0]",
"$.foo.a[0:5]",
[]interface{}{
queryTestNode{
int64(1), toml.Position{2, 1},
},
queryTestNode{
int64(2), toml.Position{2, 1},
},
queryTestNode{
int64(3), toml.Position{2, 1},
},
queryTestNode{
int64(4), toml.Position{2, 1},
},
queryTestNode{
int64(5), toml.Position{2, 1},
},
})
}
func TestQuerySliceStep(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [1,2,3,4,5,6,7,8,9,0]",
"$.foo.a[0:5:2]",
[]interface{}{
queryTestNode{
int64(1), toml.Position{2, 1},
},
queryTestNode{
int64(3), toml.Position{2, 1},
},
queryTestNode{
int64(5), toml.Position{2, 1},
},
})
}
func TestQueryAny(t *testing.T) {
assertQueryPositions(t,
"[foo.bar]\na=1\nb=2\n[foo.baz]\na=3\nb=4",
"$.foo.*",
[]interface{}{
queryTestNode{
map[string]interface{}{
"a": int64(1),
"b": int64(2),
}, toml.Position{1, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(3),
"b": int64(4),
}, toml.Position{4, 1},
},
})
}
func TestQueryUnionSimple(t *testing.T) {
assertQueryPositions(t,
"[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6",
"$.*[bar,foo]",
[]interface{}{
queryTestNode{
map[string]interface{}{
"a": int64(1),
"b": int64(2),
}, toml.Position{1, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(3),
"b": int64(4),
}, toml.Position{4, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(5),
"b": int64(6),
}, toml.Position{7, 1},
},
})
}
func TestQueryRecursionAll(t *testing.T) {
assertQueryPositions(t,
"[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6",
"$..*",
[]interface{}{
queryTestNode{
map[string]interface{}{
"foo": map[string]interface{}{
"bar": map[string]interface{}{
"a": int64(1),
"b": int64(2),
},
},
"baz": map[string]interface{}{
"foo": map[string]interface{}{
"a": int64(3),
"b": int64(4),
},
},
"gorf": map[string]interface{}{
"foo": map[string]interface{}{
"a": int64(5),
"b": int64(6),
},
},
}, toml.Position{1, 1},
},
queryTestNode{
map[string]interface{}{
"bar": map[string]interface{}{
"a": int64(1),
"b": int64(2),
},
}, toml.Position{1, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(1),
"b": int64(2),
}, toml.Position{1, 1},
},
queryTestNode{
int64(1), toml.Position{2, 1},
},
queryTestNode{
int64(2), toml.Position{3, 1},
},
queryTestNode{
map[string]interface{}{
"foo": map[string]interface{}{
"a": int64(3),
"b": int64(4),
},
}, toml.Position{4, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(3),
"b": int64(4),
}, toml.Position{4, 1},
},
queryTestNode{
int64(3), toml.Position{5, 1},
},
queryTestNode{
int64(4), toml.Position{6, 1},
},
queryTestNode{
map[string]interface{}{
"foo": map[string]interface{}{
"a": int64(5),
"b": int64(6),
},
}, toml.Position{7, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(5),
"b": int64(6),
}, toml.Position{7, 1},
},
queryTestNode{
int64(5), toml.Position{8, 1},
},
queryTestNode{
int64(6), toml.Position{9, 1},
},
})
}
func TestQueryRecursionUnionSimple(t *testing.T) {
assertQueryPositions(t,
"[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6",
"$..['foo','bar']",
[]interface{}{
queryTestNode{
map[string]interface{}{
"bar": map[string]interface{}{
"a": int64(1),
"b": int64(2),
},
}, toml.Position{1, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(3),
"b": int64(4),
}, toml.Position{4, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(1),
"b": int64(2),
}, toml.Position{1, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(5),
"b": int64(6),
}, toml.Position{7, 1},
},
})
}
func TestQueryFilterFn(t *testing.T) {
buff, err := ioutil.ReadFile("../example.toml")
if err != nil {
t.Error(err)
return
}
assertQueryPositions(t, string(buff),
"$..[?(int)]",
[]interface{}{
queryTestNode{
int64(8001), toml.Position{13, 1},
},
queryTestNode{
int64(8001), toml.Position{13, 1},
},
queryTestNode{
int64(8002), toml.Position{13, 1},
},
queryTestNode{
int64(5000), toml.Position{14, 1},
},
})
assertQueryPositions(t, string(buff),
"$..[?(string)]",
[]interface{}{
queryTestNode{
"TOML Example", toml.Position{3, 1},
},
queryTestNode{
"Tom Preston-Werner", toml.Position{6, 1},
},
queryTestNode{
"GitHub", toml.Position{7, 1},
},
queryTestNode{
"GitHub Cofounder & CEO\nLikes tater tots and beer.",
toml.Position{8, 1},
},
queryTestNode{
"192.168.1.1", toml.Position{12, 1},
},
queryTestNode{
"10.0.0.1", toml.Position{21, 3},
},
queryTestNode{
"eqdc10", toml.Position{22, 3},
},
queryTestNode{
"10.0.0.2", toml.Position{25, 3},
},
queryTestNode{
"eqdc10", toml.Position{26, 3},
},
})
assertQueryPositions(t, string(buff),
"$..[?(float)]",
[]interface{}{ // no float values in document
})
tv, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
assertQueryPositions(t, string(buff),
"$..[?(tree)]",
[]interface{}{
queryTestNode{
map[string]interface{}{
"name": "Tom Preston-Werner",
"organization": "GitHub",
"bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
"dob": tv,
}, toml.Position{5, 1},
},
queryTestNode{
map[string]interface{}{
"server": "192.168.1.1",
"ports": []interface{}{int64(8001), int64(8001), int64(8002)},
"connection_max": int64(5000),
"enabled": true,
}, toml.Position{11, 1},
},
queryTestNode{
map[string]interface{}{
"alpha": map[string]interface{}{
"ip": "10.0.0.1",
"dc": "eqdc10",
},
"beta": map[string]interface{}{
"ip": "10.0.0.2",
"dc": "eqdc10",
},
}, toml.Position{17, 1},
},
queryTestNode{
map[string]interface{}{
"ip": "10.0.0.1",
"dc": "eqdc10",
}, toml.Position{20, 3},
},
queryTestNode{
map[string]interface{}{
"ip": "10.0.0.2",
"dc": "eqdc10",
}, toml.Position{24, 3},
},
queryTestNode{
map[string]interface{}{
"data": []interface{}{
[]interface{}{"gamma", "delta"},
[]interface{}{int64(1), int64(2)},
},
}, toml.Position{28, 1},
},
})
assertQueryPositions(t, string(buff),
"$..[?(time)]",
[]interface{}{
queryTestNode{
tv, toml.Position{9, 1},
},
})
assertQueryPositions(t, string(buff),
"$..[?(bool)]",
[]interface{}{
queryTestNode{
true, toml.Position{15, 1},
},
})
}
-158
View File
@@ -1,158 +0,0 @@
package query
import (
"time"
"github.com/pelletier/go-toml"
)
// NodeFilterFn represents a user-defined filter function, for use with
// Query.SetFilter().
//
// The return value of the function must indicate if 'node' is to be included
// at this stage of the TOML path. Returning true will include the node, and
// returning false will exclude it.
//
// NOTE: Care should be taken to write script callbacks such that they are safe
// to use from multiple goroutines.
type NodeFilterFn func(node interface{}) bool
// Result is the result of Executing a Query.
type Result struct {
items []interface{}
positions []toml.Position
}
// appends a value/position pair to the result set.
func (r *Result) appendResult(node interface{}, pos toml.Position) {
r.items = append(r.items, node)
r.positions = append(r.positions, pos)
}
// Values is a set of values within a Result. The order of values is not
// guaranteed to be in document order, and may be different each time a query is
// executed.
func (r Result) Values() []interface{} {
return r.items
}
// Positions is a set of positions for values within a Result. Each index
// in Positions() corresponds to the entry in Value() of the same index.
func (r Result) Positions() []toml.Position {
return r.positions
}
// runtime context for executing query paths
type queryContext struct {
result *Result
filters *map[string]NodeFilterFn
lastPosition toml.Position
}
// generic path functor interface
type pathFn interface {
setNext(next pathFn)
// it is the caller's responsibility to set the ctx.lastPosition before invoking call()
// node can be one of: *toml.Tree, []*toml.Tree, or a scalar
call(node interface{}, ctx *queryContext)
}
// A Query is the representation of a compiled TOML path. A Query is safe
// for concurrent use by multiple goroutines.
type Query struct {
root pathFn
tail pathFn
filters *map[string]NodeFilterFn
}
func newQuery() *Query {
return &Query{
root: nil,
tail: nil,
filters: &defaultFilterFunctions,
}
}
func (q *Query) appendPath(next pathFn) {
if q.root == nil {
q.root = next
} else {
q.tail.setNext(next)
}
q.tail = next
next.setNext(newTerminatingFn()) // init the next functor
}
// Compile compiles a TOML path expression. The returned Query can be used
// to match elements within a Tree and its descendants. See Execute.
func Compile(path string) (*Query, error) {
return parseQuery(lexQuery(path))
}
// Execute executes a query against a Tree, and returns the result of the query.
func (q *Query) Execute(tree *toml.Tree) *Result {
result := &Result{
items: []interface{}{},
positions: []toml.Position{},
}
if q.root == nil {
result.appendResult(tree, tree.GetPosition(""))
} else {
ctx := &queryContext{
result: result,
filters: q.filters,
}
ctx.lastPosition = tree.Position()
q.root.call(tree, ctx)
}
return result
}
// CompileAndExecute is a shorthand for Compile(path) followed by Execute(tree).
func CompileAndExecute(path string, tree *toml.Tree) (*Result, error) {
query, err := Compile(path)
if err != nil {
return nil, err
}
return query.Execute(tree), nil
}
// SetFilter sets a user-defined filter function. These may be used inside
// "?(..)" query expressions to filter TOML document elements within a query.
func (q *Query) SetFilter(name string, fn NodeFilterFn) {
if q.filters == &defaultFilterFunctions {
// clone the static table
q.filters = &map[string]NodeFilterFn{}
for k, v := range defaultFilterFunctions {
(*q.filters)[k] = v
}
}
(*q.filters)[name] = fn
}
var defaultFilterFunctions = map[string]NodeFilterFn{
"tree": func(node interface{}) bool {
_, ok := node.(*toml.Tree)
return ok
},
"int": func(node interface{}) bool {
_, ok := node.(int64)
return ok
},
"float": func(node interface{}) bool {
_, ok := node.(float64)
return ok
},
"string": func(node interface{}) bool {
_, ok := node.(string)
return ok
},
"time": func(node interface{}) bool {
_, ok := node.(time.Time)
return ok
},
"bool": func(node interface{}) bool {
_, ok := node.(bool)
return ok
},
}
-157
View File
@@ -1,157 +0,0 @@
package query
import (
"fmt"
"testing"
"github.com/pelletier/go-toml"
)
func assertArrayContainsInAnyOrder(t *testing.T, array []interface{}, objects ...interface{}) {
if len(array) != len(objects) {
t.Fatalf("array contains %d objects but %d are expected", len(array), len(objects))
}
for _, o := range objects {
found := false
for _, a := range array {
if a == o {
found = true
break
}
}
if !found {
t.Fatal(o, "not found in array", array)
}
}
}
func TestQueryExample(t *testing.T) {
config, _ := toml.Load(`
[[book]]
title = "The Stand"
author = "Stephen King"
[[book]]
title = "For Whom the Bell Tolls"
author = "Ernest Hemmingway"
[[book]]
title = "Neuromancer"
author = "William Gibson"
`)
authors, err := CompileAndExecute("$.book.author", config)
if err != nil {
t.Fatal("unexpected error:", err)
}
names := authors.Values()
if len(names) != 3 {
t.Fatalf("query should return 3 names but returned %d", len(names))
}
assertArrayContainsInAnyOrder(t, names, "Stephen King", "Ernest Hemmingway", "William Gibson")
}
func TestQueryReadmeExample(t *testing.T) {
config, _ := toml.Load(`
[postgres]
user = "pelletier"
password = "mypassword"
`)
query, err := Compile("$..[user,password]")
if err != nil {
t.Fatal("unexpected error:", err)
}
results := query.Execute(config)
values := results.Values()
if len(values) != 2 {
t.Fatalf("query should return 2 values but returned %d", len(values))
}
assertArrayContainsInAnyOrder(t, values, "pelletier", "mypassword")
}
func TestQueryPathNotPresent(t *testing.T) {
config, _ := toml.Load(`a = "hello"`)
query, err := Compile("$.foo.bar")
if err != nil {
t.Fatal("unexpected error:", err)
}
results := query.Execute(config)
if err != nil {
t.Fatalf("err should be nil. got %s instead", err)
}
if len(results.items) != 0 {
t.Fatalf("no items should be matched. %d matched instead", len(results.items))
}
}
func ExampleNodeFilterFn_filterExample() {
tree, _ := toml.Load(`
[struct_one]
foo = "foo"
bar = "bar"
[struct_two]
baz = "baz"
gorf = "gorf"
`)
// create a query that references a user-defined-filter
query, _ := Compile("$[?(bazOnly)]")
// define the filter, and assign it to the query
query.SetFilter("bazOnly", func(node interface{}) bool {
if tree, ok := node.(*toml.Tree); ok {
return tree.Has("baz")
}
return false // reject all other node types
})
// results contain only the 'struct_two' Tree
query.Execute(tree)
}
func ExampleQuery_queryExample() {
config, _ := toml.Load(`
[[book]]
title = "The Stand"
author = "Stephen King"
[[book]]
title = "For Whom the Bell Tolls"
author = "Ernest Hemmingway"
[[book]]
title = "Neuromancer"
author = "William Gibson"
`)
// find and print all the authors in the document
query, _ := Compile("$.book.author")
authors := query.Execute(config)
for _, name := range authors.Values() {
fmt.Println(name)
}
}
func TestTomlQuery(t *testing.T) {
tree, err := toml.Load("[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6")
if err != nil {
t.Error(err)
return
}
query, err := Compile("$.foo.bar")
if err != nil {
t.Error(err)
return
}
result := query.Execute(tree)
values := result.Values()
if len(values) != 1 {
t.Errorf("Expected resultset of 1, got %d instead: %v", len(values), values)
}
if tt, ok := values[0].(*toml.Tree); !ok {
t.Errorf("Expected type of Tree: %T", values[0])
} else if tt.Get("a") != int64(1) {
t.Errorf("Expected 'a' with a value 1: %v", tt.Get("a"))
} else if tt.Get("b") != int64(2) {
t.Errorf("Expected 'b' with a value 2: %v", tt.Get("b"))
}
}
-106
View File
@@ -1,106 +0,0 @@
package query
import (
"fmt"
"github.com/pelletier/go-toml"
"strconv"
"unicode"
)
// Define tokens
type tokenType int
const (
eof = -(iota + 1)
)
const (
tokenError tokenType = iota
tokenEOF
tokenKey
tokenString
tokenInteger
tokenFloat
tokenLeftBracket
tokenRightBracket
tokenLeftParen
tokenRightParen
tokenComma
tokenColon
tokenDollar
tokenStar
tokenQuestion
tokenDot
tokenDotDot
)
var tokenTypeNames = []string{
"Error",
"EOF",
"Key",
"String",
"Integer",
"Float",
"[",
"]",
"(",
")",
",",
":",
"$",
"*",
"?",
".",
"..",
}
type token struct {
toml.Position
typ tokenType
val string
}
func (tt tokenType) String() string {
idx := int(tt)
if idx < len(tokenTypeNames) {
return tokenTypeNames[idx]
}
return "Unknown"
}
func (t token) Int() int {
if result, err := strconv.Atoi(t.val); err != nil {
panic(err)
} else {
return result
}
}
func (t token) String() string {
switch t.typ {
case tokenEOF:
return "EOF"
case tokenError:
return t.val
}
return fmt.Sprintf("%q", t.val)
}
func isSpace(r rune) bool {
return r == ' ' || r == '\t'
}
func isAlphanumeric(r rune) bool {
return unicode.IsLetter(r) || r == '_'
}
func isDigit(r rune) bool {
return unicode.IsNumber(r)
}
func isHexDigit(r rune) bool {
return isDigit(r) ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}
+173
View File
@@ -0,0 +1,173 @@
package toml
func scanFollows(b []byte, pattern string) bool {
n := len(pattern)
return len(b) >= n && string(b[:n]) == pattern
}
func scanFollowsMultilineBasicStringDelimiter(b []byte) bool {
return scanFollows(b, `"""`)
}
func scanFollowsMultilineLiteralStringDelimiter(b []byte) bool {
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++ {
if !isUnquotedKeyChar(b[i]) {
return b[:i], b[i:]
}
}
return b, b[len(b):]
}
func isUnquotedKeyChar(r byte) bool {
return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_'
}
func scanLiteralString(b []byte) ([]byte, []byte, error) {
// literal-string = apostrophe *literal-char apostrophe
// apostrophe = %x27 ; ' apostrophe
// literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
for i := 1; i < len(b); i++ {
switch b[i] {
case '\'':
return b[:i+1], b[i+1:], nil
case '\n':
return nil, nil, newDecodeError(b[i:i+1], "literal strings cannot have new lines")
}
}
return nil, nil, newDecodeError(b[len(b):], "unterminated literal string")
}
func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) {
// ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body
// ml-literal-string-delim
// ml-literal-string-delim = 3apostrophe
// ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ]
//
// mll-content = mll-char / newline
// mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
// mll-quotes = 1*2apostrophe
for i := 3; i < len(b); i++ {
if b[i] == '\'' && scanFollowsMultilineLiteralStringDelimiter(b[i:]) {
return b[:i+3], b[i+3:], nil
}
}
return nil, nil, newDecodeError(b[len(b):], `multiline literal string not terminated by '''`)
}
func scanWindowsNewline(b []byte) ([]byte, []byte, error) {
const lenCRLF = 2
if len(b) < lenCRLF {
return nil, nil, newDecodeError(b, "windows new line expected")
}
if b[1] != '\n' {
return nil, nil, newDecodeError(b, `windows new line should be \r\n`)
}
return b[:lenCRLF], b[lenCRLF:], nil
}
func scanWhitespace(b []byte) ([]byte, []byte) {
for i := 0; i < len(b); i++ {
switch b[i] {
case ' ', '\t':
continue
default:
return b[:i], b[i:]
}
}
return b, b[len(b):]
}
//nolint:unparam
func scanComment(b []byte) ([]byte, []byte) {
// comment-start-symbol = %x23 ; #
// 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++ {
if b[i] == '\n' {
return b[:i], b[i:]
}
}
return b, nil
}
func scanBasicString(b []byte) ([]byte, []byte, error) {
// basic-string = quotation-mark *basic-char quotation-mark
// quotation-mark = %x22 ; "
// basic-char = basic-unescaped / escaped
// basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// escaped = escape escape-seq-char
for i := 1; i < len(b); i++ {
switch b[i] {
case '"':
return b[:i+1], b[i+1:], nil
case '\n':
return nil, nil, newDecodeError(b[i:i+1], "basic strings cannot have new lines")
case '\\':
if len(b) < i+2 {
return nil, nil, newDecodeError(b[i:i+1], "need a character after \\")
}
i++ // skip the next character
}
}
return nil, nil, newDecodeError(b[len(b):], `basic string not terminated by "`)
}
func scanMultilineBasicString(b []byte) ([]byte, []byte, error) {
// ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
// ml-basic-string-delim
// ml-basic-string-delim = 3quotation-mark
// ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ]
//
// mlb-content = mlb-char / newline / mlb-escaped-nl
// mlb-char = mlb-unescaped / escaped
// mlb-quotes = 1*2quotation-mark
// mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// mlb-escaped-nl = escape ws newline *( wschar / newline )
for i := 3; i < len(b); i++ {
switch b[i] {
case '"':
if scanFollowsMultilineBasicStringDelimiter(b[i:]) {
return b[:i+3], b[i+3:], nil
}
case '\\':
if len(b) < i+2 {
return nil, nil, newDecodeError(b[len(b):], "need a character after \\")
}
i++ // skip the next character
}
}
return nil, nil, newDecodeError(b[len(b):], `multiline basic string not terminated by """`)
}
+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)
}
-137
View File
@@ -1,137 +0,0 @@
package toml
import (
"fmt"
"unicode"
)
// Define tokens
type tokenType int
const (
eof = -(iota + 1)
)
const (
tokenError tokenType = iota
tokenEOF
tokenComment
tokenKey
tokenString
tokenInteger
tokenTrue
tokenFalse
tokenFloat
tokenInf
tokenNan
tokenEqual
tokenLeftBracket
tokenRightBracket
tokenLeftCurlyBrace
tokenRightCurlyBrace
tokenLeftParen
tokenRightParen
tokenDoubleLeftBracket
tokenDoubleRightBracket
tokenDate
tokenLocalDate
tokenKeyGroup
tokenKeyGroupArray
tokenComma
tokenColon
tokenDollar
tokenStar
tokenQuestion
tokenDot
tokenDotDot
tokenEOL
)
var tokenTypeNames = []string{
"Error",
"EOF",
"Comment",
"Key",
"String",
"Integer",
"True",
"False",
"Float",
"Inf",
"NaN",
"=",
"[",
"]",
"{",
"}",
"(",
")",
"]]",
"[[",
"LocalDate",
"LocalDate",
"KeyGroup",
"KeyGroupArray",
",",
":",
"$",
"*",
"?",
".",
"..",
"EOL",
}
type token struct {
Position
typ tokenType
val string
}
func (tt tokenType) String() string {
idx := int(tt)
if idx < len(tokenTypeNames) {
return tokenTypeNames[idx]
}
return "Unknown"
}
func (t token) String() string {
switch t.typ {
case tokenEOF:
return "EOF"
case tokenError:
return t.val
}
return fmt.Sprintf("%q", t.val)
}
func isSpace(r rune) bool {
return r == ' ' || r == '\t'
}
func isAlphanumeric(r rune) bool {
return unicode.IsLetter(r) || r == '_'
}
func isKeyChar(r rune) bool {
// Keys start with the first character that isn't whitespace or [ and end
// with the last non-whitespace character before the equals sign. Keys
// cannot contain a # character."
return !(r == '\r' || r == '\n' || r == eof || r == '=')
}
func isKeyStartChar(r rune) bool {
return !(isSpace(r) || r == '\r' || r == '\n' || r == eof || r == '[')
}
func isDigit(r rune) bool {
return unicode.IsNumber(r)
}
func isHexDigit(r rune) bool {
return isDigit(r) ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}
-68
View File
@@ -1,68 +0,0 @@
package toml
import "testing"
func TestTokenStringer(t *testing.T) {
var tests = []struct {
tt tokenType
expect string
}{
{tokenError, "Error"},
{tokenEOF, "EOF"},
{tokenComment, "Comment"},
{tokenKey, "Key"},
{tokenString, "String"},
{tokenInteger, "Integer"},
{tokenTrue, "True"},
{tokenFalse, "False"},
{tokenFloat, "Float"},
{tokenEqual, "="},
{tokenLeftBracket, "["},
{tokenRightBracket, "]"},
{tokenLeftCurlyBrace, "{"},
{tokenRightCurlyBrace, "}"},
{tokenLeftParen, "("},
{tokenRightParen, ")"},
{tokenDoubleLeftBracket, "]]"},
{tokenDoubleRightBracket, "[["},
{tokenDate, "LocalDate"},
{tokenLocalDate, "LocalDate"},
{tokenKeyGroup, "KeyGroup"},
{tokenKeyGroupArray, "KeyGroupArray"},
{tokenComma, ","},
{tokenColon, ":"},
{tokenDollar, "$"},
{tokenStar, "*"},
{tokenQuestion, "?"},
{tokenDot, "."},
{tokenDotDot, ".."},
{tokenEOL, "EOL"},
{tokenEOL + 1, "Unknown"},
}
for i, test := range tests {
got := test.tt.String()
if got != test.expect {
t.Errorf("[%d] invalid string of token type; got %q, expected %q", i, got, test.expect)
}
}
}
func TestTokenString(t *testing.T) {
var tests = []struct {
tok token
expect string
}{
{token{Position{1, 1}, tokenEOF, ""}, "EOF"},
{token{Position{1, 1}, tokenError, "Δt"}, "Δt"},
{token{Position{1, 1}, tokenString, "bar"}, `"bar"`},
{token{Position{1, 1}, tokenString, "123456789012345"}, `"123456789012345"`},
}
for i, test := range tests {
got := test.tok.String()
if got != test.expect {
t.Errorf("[%d] invalid of string token; got %q, expected %q", i, got, test.expect)
}
}
}
+243
View File
@@ -0,0 +1,243 @@
;; This document describes TOML's syntax, using the ABNF format (defined in
;; RFC 5234 -- https://www.ietf.org/rfc/rfc5234.txt).
;;
;; All valid TOML documents will match this description, however certain
;; invalid documents would need to be rejected as per the semantics described
;; in the supporting text description.
;; It is possible to try this grammar interactively, using instaparse.
;; http://instaparse.mojombo.com/
;;
;; To do so, in the lower right, click on Options and change `:input-format` to
;; ':abnf'. Then paste this entire ABNF document into the grammar entry box
;; (above the options). Then you can type or paste a sample TOML document into
;; the beige box on the left. Tada!
;; Overall Structure
toml = expression *( newline expression )
expression = ws [ comment ]
expression =/ ws keyval ws [ comment ]
expression =/ ws table ws [ comment ]
;; Whitespace
ws = *wschar
wschar = %x20 ; Space
wschar =/ %x09 ; Horizontal tab
;; Newline
newline = %x0A ; LF
newline =/ %x0D.0A ; CRLF
;; Comment
comment-start-symbol = %x23 ; #
non-ascii = %x80-D7FF / %xE000-10FFFF
non-eol = %x09 / %x20-7F / non-ascii
comment = comment-start-symbol *non-eol
;; Key-Value pairs
keyval = key keyval-sep val
key = simple-key / dotted-key
simple-key = quoted-key / unquoted-key
unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
quoted-key = basic-string / literal-string
dotted-key = simple-key 1*( dot-sep simple-key )
dot-sep = ws %x2E ws ; . Period
keyval-sep = ws %x3D ws ; =
val = string / boolean / array / inline-table / date-time / float / integer
;; String
string = ml-basic-string / basic-string / ml-literal-string / literal-string
;; Basic String
basic-string = quotation-mark *basic-char quotation-mark
quotation-mark = %x22 ; "
basic-char = basic-unescaped / escaped
basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
escaped = escape escape-seq-char
escape = %x5C ; \
escape-seq-char = %x22 ; " quotation mark U+0022
escape-seq-char =/ %x5C ; \ reverse solidus U+005C
escape-seq-char =/ %x62 ; b backspace U+0008
escape-seq-char =/ %x66 ; f form feed U+000C
escape-seq-char =/ %x6E ; n line feed U+000A
escape-seq-char =/ %x72 ; r carriage return U+000D
escape-seq-char =/ %x74 ; t tab U+0009
escape-seq-char =/ %x75 4HEXDIG ; uXXXX U+XXXX
escape-seq-char =/ %x55 8HEXDIG ; UXXXXXXXX U+XXXXXXXX
;; Multiline Basic String
ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
ml-basic-string-delim
ml-basic-string-delim = 3quotation-mark
ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ]
mlb-content = mlb-char / newline / mlb-escaped-nl
mlb-char = mlb-unescaped / escaped
mlb-quotes = 1*2quotation-mark
mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
mlb-escaped-nl = escape ws newline *( wschar / newline )
;; Literal String
literal-string = apostrophe *literal-char apostrophe
apostrophe = %x27 ; ' apostrophe
literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
;; Multiline Literal String
ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body
ml-literal-string-delim
ml-literal-string-delim = 3apostrophe
ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ]
mll-content = mll-char / newline
mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
mll-quotes = 1*2apostrophe
;; Integer
integer = dec-int / hex-int / oct-int / bin-int
minus = %x2D ; -
plus = %x2B ; +
underscore = %x5F ; _
digit1-9 = %x31-39 ; 1-9
digit0-7 = %x30-37 ; 0-7
digit0-1 = %x30-31 ; 0-1
hex-prefix = %x30.78 ; 0x
oct-prefix = %x30.6F ; 0o
bin-prefix = %x30.62 ; 0b
dec-int = [ minus / plus ] unsigned-dec-int
unsigned-dec-int = DIGIT / digit1-9 1*( DIGIT / underscore DIGIT )
hex-int = hex-prefix HEXDIG *( HEXDIG / underscore HEXDIG )
oct-int = oct-prefix digit0-7 *( digit0-7 / underscore digit0-7 )
bin-int = bin-prefix digit0-1 *( digit0-1 / underscore digit0-1 )
;; Float
float = float-int-part ( exp / frac [ exp ] )
float =/ special-float
float-int-part = dec-int
frac = decimal-point zero-prefixable-int
decimal-point = %x2E ; .
zero-prefixable-int = DIGIT *( DIGIT / underscore DIGIT )
exp = "e" float-exp-part
float-exp-part = [ minus / plus ] zero-prefixable-int
special-float = [ minus / plus ] ( inf / nan )
inf = %x69.6e.66 ; inf
nan = %x6e.61.6e ; nan
;; Boolean
boolean = true / false
true = %x74.72.75.65 ; true
false = %x66.61.6C.73.65 ; false
;; Date and Time (as defined in RFC 3339)
date-time = offset-date-time / local-date-time / local-date / local-time
date-fullyear = 4DIGIT
date-month = 2DIGIT ; 01-12
date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
time-delim = "T" / %x20 ; T, t, or space
time-hour = 2DIGIT ; 00-23
time-minute = 2DIGIT ; 00-59
time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second rules
time-secfrac = "." 1*DIGIT
time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
time-offset = "Z" / time-numoffset
partial-time = time-hour ":" time-minute ":" time-second [ time-secfrac ]
full-date = date-fullyear "-" date-month "-" date-mday
full-time = partial-time time-offset
;; Offset Date-Time
offset-date-time = full-date time-delim full-time
;; Local Date-Time
local-date-time = full-date time-delim partial-time
;; Local Date
local-date = full-date
;; Local Time
local-time = partial-time
;; Array
array = array-open [ array-values ] ws-comment-newline array-close
array-open = %x5B ; [
array-close = %x5D ; ]
array-values = ws-comment-newline val ws-comment-newline array-sep array-values
array-values =/ ws-comment-newline val ws-comment-newline [ array-sep ]
array-sep = %x2C ; , Comma
ws-comment-newline = *( wschar / [ comment ] newline )
;; Table
table = std-table / array-table
;; Standard Table
std-table = std-table-open key std-table-close
std-table-open = %x5B ws ; [ Left square bracket
std-table-close = ws %x5D ; ] Right square bracket
;; Inline Table
inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close
inline-table-open = %x7B ws ; {
inline-table-close = ws %x7D ; }
inline-table-sep = ws %x2C ws ; , Comma
inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
;; Array Table
array-table = array-table-open key array-table-close
array-table-open = %x5B.5B ws ; [[ Double left square bracket
array-table-close = ws %x5D.5D ; ]] Double right square bracket
;; Built-in ABNF terms, reproduced here for clarity
ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
DIGIT = %x30-39 ; 0-9
HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
-393
View File
@@ -1,393 +0,0 @@
package toml
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"runtime"
"strings"
)
type tomlValue struct {
value interface{} // string, int64, uint64, float64, bool, time.Time, [] of any of this list
comment string
commented bool
multiline bool
position Position
}
// Tree is the result of the parsing of a TOML file.
type Tree struct {
values map[string]interface{} // string -> *tomlValue, *Tree, []*Tree
comment string
commented bool
position Position
}
func newTree() *Tree {
return newTreeWithPosition(Position{})
}
func newTreeWithPosition(pos Position) *Tree {
return &Tree{
values: make(map[string]interface{}),
position: pos,
}
}
// TreeFromMap initializes a new Tree object using the given map.
func TreeFromMap(m map[string]interface{}) (*Tree, error) {
result, err := toTree(m)
if err != nil {
return nil, err
}
return result.(*Tree), nil
}
// Position returns the position of the tree.
func (t *Tree) Position() Position {
return t.position
}
// Has returns a boolean indicating if the given key exists.
func (t *Tree) Has(key string) bool {
if key == "" {
return false
}
return t.HasPath(strings.Split(key, "."))
}
// HasPath returns true if the given path of keys exists, false otherwise.
func (t *Tree) HasPath(keys []string) bool {
return t.GetPath(keys) != nil
}
// Keys returns the keys of the toplevel tree (does not recurse).
func (t *Tree) Keys() []string {
keys := make([]string, len(t.values))
i := 0
for k := range t.values {
keys[i] = k
i++
}
return keys
}
// Get the value at key in the Tree.
// Key is a dot-separated path (e.g. a.b.c) without single/double quoted strings.
// If you need to retrieve non-bare keys, use GetPath.
// Returns nil if the path does not exist in the tree.
// If keys is of length zero, the current tree is returned.
func (t *Tree) Get(key string) interface{} {
if key == "" {
return t
}
return t.GetPath(strings.Split(key, "."))
}
// GetPath returns the element in the tree indicated by 'keys'.
// If keys is of length zero, the current tree is returned.
func (t *Tree) GetPath(keys []string) interface{} {
if len(keys) == 0 {
return t
}
subtree := t
for _, intermediateKey := range keys[:len(keys)-1] {
value, exists := subtree.values[intermediateKey]
if !exists {
return nil
}
switch node := value.(type) {
case *Tree:
subtree = node
case []*Tree:
// go to most recent element
if len(node) == 0 {
return nil
}
subtree = node[len(node)-1]
default:
return nil // cannot navigate through other node types
}
}
// branch based on final node type
switch node := subtree.values[keys[len(keys)-1]].(type) {
case *tomlValue:
return node.value
default:
return node
}
}
// GetPosition returns the position of the given key.
func (t *Tree) GetPosition(key string) Position {
if key == "" {
return t.position
}
return t.GetPositionPath(strings.Split(key, "."))
}
// GetPositionPath returns the element in the tree indicated by 'keys'.
// If keys is of length zero, the current tree is returned.
func (t *Tree) GetPositionPath(keys []string) Position {
if len(keys) == 0 {
return t.position
}
subtree := t
for _, intermediateKey := range keys[:len(keys)-1] {
value, exists := subtree.values[intermediateKey]
if !exists {
return Position{0, 0}
}
switch node := value.(type) {
case *Tree:
subtree = node
case []*Tree:
// go to most recent element
if len(node) == 0 {
return Position{0, 0}
}
subtree = node[len(node)-1]
default:
return Position{0, 0}
}
}
// branch based on final node type
switch node := subtree.values[keys[len(keys)-1]].(type) {
case *tomlValue:
return node.position
case *Tree:
return node.position
case []*Tree:
// go to most recent element
if len(node) == 0 {
return Position{0, 0}
}
return node[len(node)-1].position
default:
return Position{0, 0}
}
}
// GetDefault works like Get but with a default value
func (t *Tree) GetDefault(key string, def interface{}) interface{} {
val := t.Get(key)
if val == nil {
return def
}
return val
}
// SetOptions arguments are supplied to the SetWithOptions and SetPathWithOptions functions to modify marshalling behaviour.
// The default values within the struct are valid default options.
type SetOptions struct {
Comment string
Commented bool
Multiline bool
}
// SetWithOptions is the same as Set, but allows you to provide formatting
// instructions to the key, that will be used by Marshal().
func (t *Tree) SetWithOptions(key string, opts SetOptions, value interface{}) {
t.SetPathWithOptions(strings.Split(key, "."), opts, value)
}
// SetPathWithOptions is the same as SetPath, but allows you to provide
// formatting instructions to the key, that will be reused by Marshal().
func (t *Tree) SetPathWithOptions(keys []string, opts SetOptions, value interface{}) {
subtree := t
for i, intermediateKey := range keys[:len(keys)-1] {
nextTree, exists := subtree.values[intermediateKey]
if !exists {
nextTree = newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col})
subtree.values[intermediateKey] = nextTree // add new element here
}
switch node := nextTree.(type) {
case *Tree:
subtree = node
case []*Tree:
// go to most recent element
if len(node) == 0 {
// create element if it does not exist
subtree.values[intermediateKey] = append(node, newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col}))
}
subtree = node[len(node)-1]
}
}
var toInsert interface{}
switch v := value.(type) {
case *Tree:
v.comment = opts.Comment
toInsert = value
case []*Tree:
toInsert = value
case *tomlValue:
v.comment = opts.Comment
toInsert = v
default:
toInsert = &tomlValue{value: value,
comment: opts.Comment,
commented: opts.Commented,
multiline: opts.Multiline,
position: Position{Line: subtree.position.Line + len(subtree.values) + 1, Col: subtree.position.Col}}
}
subtree.values[keys[len(keys)-1]] = toInsert
}
// Set an element in the tree.
// Key is a dot-separated path (e.g. a.b.c).
// Creates all necessary intermediate trees, if needed.
func (t *Tree) Set(key string, value interface{}) {
t.SetWithComment(key, "", false, value)
}
// SetWithComment is the same as Set, but allows you to provide comment
// information to the key, that will be reused by Marshal().
func (t *Tree) SetWithComment(key string, comment string, commented bool, value interface{}) {
t.SetPathWithComment(strings.Split(key, "."), comment, commented, value)
}
// SetPath sets an element in the tree.
// Keys is an array of path elements (e.g. {"a","b","c"}).
// Creates all necessary intermediate trees, if needed.
func (t *Tree) SetPath(keys []string, value interface{}) {
t.SetPathWithComment(keys, "", false, value)
}
// SetPathWithComment is the same as SetPath, but allows you to provide comment
// information to the key, that will be reused by Marshal().
func (t *Tree) SetPathWithComment(keys []string, comment string, commented bool, value interface{}) {
t.SetPathWithOptions(keys, SetOptions{Comment: comment, Commented: commented}, value)
}
// Delete removes a key from the tree.
// Key is a dot-separated path (e.g. a.b.c).
func (t *Tree) Delete(key string) error {
keys, err := parseKey(key)
if err != nil {
return err
}
return t.DeletePath(keys)
}
// DeletePath removes a key from the tree.
// Keys is an array of path elements (e.g. {"a","b","c"}).
func (t *Tree) DeletePath(keys []string) error {
keyLen := len(keys)
if keyLen == 1 {
delete(t.values, keys[0])
return nil
}
tree := t.GetPath(keys[:keyLen-1])
item := keys[keyLen-1]
switch node := tree.(type) {
case *Tree:
delete(node.values, item)
return nil
}
return errors.New("no such key to delete")
}
// createSubTree takes a tree and a key and create the necessary intermediate
// subtrees to create a subtree at that point. In-place.
//
// e.g. passing a.b.c will create (assuming tree is empty) tree[a], tree[a][b]
// and tree[a][b][c]
//
// Returns nil on success, error object on failure
func (t *Tree) createSubTree(keys []string, pos Position) error {
subtree := t
for i, intermediateKey := range keys {
nextTree, exists := subtree.values[intermediateKey]
if !exists {
tree := newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col})
tree.position = pos
subtree.values[intermediateKey] = tree
nextTree = tree
}
switch node := nextTree.(type) {
case []*Tree:
subtree = node[len(node)-1]
case *Tree:
subtree = node
default:
return fmt.Errorf("unknown type for path %s (%s): %T (%#v)",
strings.Join(keys, "."), intermediateKey, nextTree, nextTree)
}
}
return nil
}
// LoadBytes creates a Tree from a []byte.
func LoadBytes(b []byte) (tree *Tree, err error) {
defer func() {
if r := recover(); r != nil {
if _, ok := r.(runtime.Error); ok {
panic(r)
}
err = errors.New(r.(string))
}
}()
if len(b) >= 4 && (hasUTF32BigEndianBOM4(b) || hasUTF32LittleEndianBOM4(b)) {
b = b[4:]
} else if len(b) >= 3 && hasUTF8BOM3(b) {
b = b[3:]
} else if len(b) >= 2 && (hasUTF16BigEndianBOM2(b) || hasUTF16LittleEndianBOM2(b)) {
b = b[2:]
}
tree = parseToml(lexToml(b))
return
}
func hasUTF16BigEndianBOM2(b []byte) bool {
return b[0] == 0xFE && b[1] == 0xFF
}
func hasUTF16LittleEndianBOM2(b []byte) bool {
return b[0] == 0xFF && b[1] == 0xFE
}
func hasUTF8BOM3(b []byte) bool {
return b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF
}
func hasUTF32BigEndianBOM4(b []byte) bool {
return b[0] == 0x00 && b[1] == 0x00 && b[2] == 0xFE && b[3] == 0xFF
}
func hasUTF32LittleEndianBOM4(b []byte) bool {
return b[0] == 0xFF && b[1] == 0xFE && b[2] == 0x00 && b[3] == 0x00
}
// LoadReader creates a Tree from any io.Reader.
func LoadReader(reader io.Reader) (tree *Tree, err error) {
inputBytes, err := ioutil.ReadAll(reader)
if err != nil {
return
}
tree, err = LoadBytes(inputBytes)
return
}
// Load creates a Tree from a string.
func Load(content string) (tree *Tree, err error) {
return LoadBytes([]byte(content))
}
// LoadFile creates a Tree from a file.
func LoadFile(path string) (tree *Tree, err error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
return LoadReader(file)
}
-180
View File
@@ -1,180 +0,0 @@
// Testing support for go-toml
package toml
import (
"testing"
)
func TestTomlHas(t *testing.T) {
tree, _ := Load(`
[test]
key = "value"
`)
if !tree.Has("test.key") {
t.Errorf("Has - expected test.key to exists")
}
if tree.Has("") {
t.Errorf("Should return false if the key is not provided")
}
}
func TestTomlGet(t *testing.T) {
tree, _ := Load(`
[test]
key = "value"
`)
if tree.Get("") != tree {
t.Errorf("Get should return the tree itself when given an empty path")
}
if tree.Get("test.key") != "value" {
t.Errorf("Get should return the value")
}
if tree.Get(`\`) != nil {
t.Errorf("should return nil when the key is malformed")
}
}
func TestTomlGetDefault(t *testing.T) {
tree, _ := Load(`
[test]
key = "value"
`)
if tree.GetDefault("", "hello") != tree {
t.Error("GetDefault should return the tree itself when given an empty path")
}
if tree.GetDefault("test.key", "hello") != "value" {
t.Error("Get should return the value")
}
if tree.GetDefault("whatever", "hello") != "hello" {
t.Error("GetDefault should return the default value if the key does not exist")
}
}
func TestTomlHasPath(t *testing.T) {
tree, _ := Load(`
[test]
key = "value"
`)
if !tree.HasPath([]string{"test", "key"}) {
t.Errorf("HasPath - expected test.key to exists")
}
}
func TestTomlDelete(t *testing.T) {
tree, _ := Load(`
key = "value"
`)
err := tree.Delete("key")
if err != nil {
t.Errorf("Delete - unexpected error while deleting key: %s", err.Error())
}
if tree.Get("key") != nil {
t.Errorf("Delete should have removed key but did not.")
}
}
func TestTomlDeleteUnparsableKey(t *testing.T) {
tree, _ := Load(`
key = "value"
`)
err := tree.Delete(".")
if err == nil {
t.Errorf("Delete should error")
}
}
func TestTomlDeleteNestedKey(t *testing.T) {
tree, _ := Load(`
[foo]
[foo.bar]
key = "value"
`)
err := tree.Delete("foo.bar.key")
if err != nil {
t.Errorf("Error while deleting nested key: %s", err.Error())
}
if tree.Get("key") != nil {
t.Errorf("Delete should have removed nested key but did not.")
}
}
func TestTomlDeleteNonexistentNestedKey(t *testing.T) {
tree, _ := Load(`
[foo]
[foo.bar]
key = "value"
`)
err := tree.Delete("foo.not.there.key")
if err == nil {
t.Errorf("Delete should have thrown an error trying to delete key in nonexistent tree")
}
}
func TestTomlGetPath(t *testing.T) {
node := newTree()
//TODO: set other node data
for idx, item := range []struct {
Path []string
Expected *Tree
}{
{ // empty path test
[]string{},
node,
},
} {
result := node.GetPath(item.Path)
if result != item.Expected {
t.Errorf("GetPath[%d] %v - expected %v, got %v instead.", idx, item.Path, item.Expected, result)
}
}
tree, _ := Load("[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6")
if tree.GetPath([]string{"whatever"}) != nil {
t.Error("GetPath should return nil when the key does not exist")
}
}
func TestTomlFromMap(t *testing.T) {
simpleMap := map[string]interface{}{"hello": 42}
tree, err := TreeFromMap(simpleMap)
if err != nil {
t.Fatal("unexpected error:", err)
}
if tree.Get("hello") != int64(42) {
t.Fatal("hello should be 42, not", tree.Get("hello"))
}
}
func TestLoadBytesBOM(t *testing.T) {
payloads := [][]byte{
[]byte("\xFE\xFFhello=1"),
[]byte("\xFF\xFEhello=1"),
[]byte("\xEF\xBB\xBFhello=1"),
[]byte("\x00\x00\xFE\xFFhello=1"),
[]byte("\xFF\xFE\x00\x00hello=1"),
}
for _, data := range payloads {
tree, err := LoadBytes(data)
if err != nil {
t.Fatal("unexpected error:", err, "for:", data)
}
v := tree.Get("hello")
if v != int64(1) {
t.Fatal("hello should be 1, not", v)
}
}
}
+119 -86
View File
@@ -1,119 +1,152 @@
// This is a support file for toml_testgen_test.go
package toml
package toml_test
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strconv"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require"
)
func testgenInvalid(t *testing.T, input string) {
t.Helper()
t.Logf("Input TOML:\n%s", input)
tree, err := Load(input)
if err != nil {
return
doc := map[string]interface{}{}
err := toml.Unmarshal([]byte(input), &doc)
if err == nil {
t.Log(json.Marshal(doc))
t.Fatalf("test did not fail")
}
typedTree := testgenTranslate(*tree)
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(typedTree); err != nil {
return
}
t.Fatalf("test did not fail. resulting tree:\n%s", buf.String())
}
func testgenValid(t *testing.T, input string, jsonRef string) {
t.Helper()
t.Logf("Input TOML:\n%s", input)
tree, err := Load(input)
doc := map[string]interface{}{}
err := toml.Unmarshal([]byte(input), &doc)
if err != nil {
t.Fatalf("failed parsing toml: %s", err)
}
typedTree := testgenTranslate(*tree)
refDoc := testgenBuildRefDoc(jsonRef)
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(typedTree); err != nil {
t.Fatalf("failed translating to JSON: %s", err)
}
require.Equal(t, refDoc, doc)
var jsonTest interface{}
if err := json.NewDecoder(buf).Decode(&jsonTest); err != nil {
t.Logf("translated JSON:\n%s", buf.String())
t.Fatalf("failed decoding translated JSON: %s", err)
}
out, err := toml.Marshal(doc)
require.NoError(t, err)
var jsonExpected interface{}
if err := json.NewDecoder(bytes.NewBufferString(jsonRef)).Decode(&jsonExpected); err != nil {
t.Logf("reference JSON:\n%s", jsonRef)
t.Fatalf("failed decoding reference JSON: %s", err)
}
doc2 := map[string]interface{}{}
err = toml.Unmarshal(out, &doc2)
require.NoError(t, err)
if !reflect.DeepEqual(jsonExpected, jsonTest) {
t.Logf("Diff:\n%s", spew.Sdump(jsonExpected, jsonTest))
t.Fatal("parsed TOML tree is different than expected structure")
}
require.Equal(t, refDoc, doc2)
}
func testgenTranslate(tomlData interface{}) interface{} {
switch orig := tomlData.(type) {
case map[string]interface{}:
typed := make(map[string]interface{}, len(orig))
for k, v := range orig {
typed[k] = testgenTranslate(v)
}
return typed
case *Tree:
return testgenTranslate(*orig)
case Tree:
keys := orig.Keys()
typed := make(map[string]interface{}, len(keys))
for _, k := range keys {
typed[k] = testgenTranslate(orig.GetPath([]string{k}))
}
return typed
case []*Tree:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = testgenTranslate(v).(map[string]interface{})
}
return typed
case []map[string]interface{}:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = testgenTranslate(v).(map[string]interface{})
}
return typed
case []interface{}:
typed := make([]interface{}, len(orig))
for i, v := range orig {
typed[i] = testgenTranslate(v)
}
return testgenTag("array", typed)
case time.Time:
return testgenTag("datetime", orig.Format("2006-01-02T15:04:05Z"))
case bool:
return testgenTag("bool", fmt.Sprintf("%v", orig))
case int64:
return testgenTag("integer", fmt.Sprintf("%d", orig))
case float64:
return testgenTag("float", fmt.Sprintf("%v", orig))
case string:
return testgenTag("string", orig)
func testgenBuildRefDoc(jsonRef string) map[string]interface{} {
descTree := map[string]interface{}{}
err := json.Unmarshal([]byte(jsonRef), &descTree)
if err != nil {
panic(fmt.Sprintf("reference doc should be valid JSON: %s", err))
}
panic(fmt.Sprintf("Unknown type: %T", tomlData))
doc := testGenTranslateDesc(descTree)
if doc == nil {
return map[string]interface{}{}
}
return doc.(map[string]interface{})
}
func testgenTag(typeName string, data interface{}) map[string]interface{} {
return map[string]interface{}{
"type": typeName,
"value": data,
//nolint:funlen,gocognit,cyclop
func testGenTranslateDesc(input interface{}) interface{} {
a, ok := input.([]interface{})
if ok {
xs := make([]interface{}, len(a))
for i, v := range a {
xs[i] = testGenTranslateDesc(v)
}
return xs
}
d, ok := input.(map[string]interface{})
if !ok {
panic(fmt.Sprintf("input should be valid map[string]: %v", input))
}
var (
dtype string
dvalue interface{}
)
//nolint:nestif
if len(d) == 2 {
dtypeiface, ok := d["type"]
if ok {
dvalue, ok = d["value"]
if ok {
dtype = dtypeiface.(string)
switch dtype {
case "string":
return dvalue.(string)
case "float":
v, err := strconv.ParseFloat(dvalue.(string), 64)
if err != nil {
panic(fmt.Sprintf("invalid float '%s': %s", dvalue, err))
}
return v
case "integer":
v, err := strconv.ParseInt(dvalue.(string), 10, 64)
if err != nil {
panic(fmt.Sprintf("invalid int '%s': %s", dvalue, err))
}
return v
case "bool":
return dvalue.(string) == "true"
case "datetime":
dt, err := time.Parse("2006-01-02T15:04:05Z", dvalue.(string))
if err != nil {
panic(fmt.Sprintf("invalid datetime '%s': %s", dvalue, err))
}
return dt
case "array":
if dvalue == nil {
return nil
}
a := dvalue.([]interface{})
xs := make([]interface{}, len(a))
for i, v := range a {
xs[i] = testGenTranslateDesc(v)
}
return xs
}
panic(fmt.Sprintf("unknown type: %s", dtype))
}
}
}
dest := map[string]interface{}{}
for k, v := range d {
dest[k] = testGenTranslateDesc(v)
}
return dest
}
+77 -18
View File
@@ -1,46 +1,36 @@
// Generated by tomltestgen for toml-test ref 39e37e6 on 2019-03-19T23:58:45-07:00
package toml
package toml_test
import (
"testing"
)
func TestInvalidArrayMixedTypesArraysAndInts(t *testing.T) {
input := `arrays-and-ints = [1, ["Arrays are not integers."]]`
testgenInvalid(t, input)
}
func TestInvalidArrayMixedTypesIntsAndFloats(t *testing.T) {
input := `ints-and-floats = [1, 1.1]`
testgenInvalid(t, input)
}
func TestInvalidArrayMixedTypesStringsAndInts(t *testing.T) {
input := `strings-and-ints = ["hi", 42]`
testgenInvalid(t, input)
}
func TestInvalidDatetimeMalformedNoLeads(t *testing.T) {
input := `no-leads = 1987-7-05T17:45:00Z`
testgenInvalid(t, input)
}
func TestInvalidDatetimeMalformedNoSecs(t *testing.T) {
input := `no-secs = 1987-07-05T17:45Z`
testgenInvalid(t, input)
}
func TestInvalidDatetimeMalformedNoT(t *testing.T) {
input := `no-t = 1987-07-0517:45:00Z`
testgenInvalid(t, input)
}
func TestInvalidDatetimeMalformedWithMilli(t *testing.T) {
input := `with-milli = 1987-07-5T17:45:00.12Z`
testgenInvalid(t, input)
}
func TestInvalidDuplicateKeyTable(t *testing.T) {
input := `[fruit]
type = "apple"
@@ -50,71 +40,84 @@ apple = "yes"`
}
func TestInvalidDuplicateKeys(t *testing.T) {
input := `dupe = false
dupe = true`
testgenInvalid(t, input)
}
func TestInvalidDuplicateTables(t *testing.T) {
input := `[a]
[a]`
testgenInvalid(t, input)
}
func TestInvalidEmptyImplicitTable(t *testing.T) {
input := `[naughty..naughty]`
testgenInvalid(t, input)
}
func TestInvalidEmptyTable(t *testing.T) {
input := `[]`
testgenInvalid(t, input)
}
func TestInvalidFloatNoLeadingZero(t *testing.T) {
input := `answer = .12345
neganswer = -.12345`
testgenInvalid(t, input)
}
func TestInvalidFloatNoTrailingDigits(t *testing.T) {
input := `answer = 1.
neganswer = -1.`
testgenInvalid(t, input)
}
func TestInvalidKeyEmpty(t *testing.T) {
input := ` = 1`
testgenInvalid(t, input)
}
func TestInvalidKeyHash(t *testing.T) {
input := `a# = 1`
testgenInvalid(t, input)
}
func TestInvalidKeyNewline(t *testing.T) {
input := `a
= 1`
testgenInvalid(t, input)
}
func TestInvalidKeyOpenBracket(t *testing.T) {
input := `[abc = 1`
testgenInvalid(t, input)
}
func TestInvalidKeySingleOpenBracket(t *testing.T) {
input := `[`
testgenInvalid(t, input)
}
func TestInvalidKeySpace(t *testing.T) {
input := `a b = 1`
testgenInvalid(t, input)
}
func TestInvalidKeyStartBracket(t *testing.T) {
input := `[a]
[xyz = 5
[b]`
@@ -122,31 +125,37 @@ func TestInvalidKeyStartBracket(t *testing.T) {
}
func TestInvalidKeyTwoEquals(t *testing.T) {
input := `key= = 1`
testgenInvalid(t, input)
}
func TestInvalidStringBadByteEscape(t *testing.T) {
input := `naughty = "\xAg"`
testgenInvalid(t, input)
}
func TestInvalidStringBadEscape(t *testing.T) {
input := `invalid-escape = "This string has a bad \a escape character."`
testgenInvalid(t, input)
}
func TestInvalidStringByteEscapes(t *testing.T) {
input := `answer = "\x33"`
testgenInvalid(t, input)
}
func TestInvalidStringNoClose(t *testing.T) {
input := `no-ending-quote = "One time, at band camp`
testgenInvalid(t, input)
}
func TestInvalidTableArrayImplicit(t *testing.T) {
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" +
"# must be a table. The alternative would be quite weird. Namely, it wouldn't\n" +
@@ -165,46 +174,54 @@ func TestInvalidTableArrayImplicit(t *testing.T) {
}
func TestInvalidTableArrayMalformedBracket(t *testing.T) {
input := `[[albums]
name = "Born to Run"`
testgenInvalid(t, input)
}
func TestInvalidTableArrayMalformedEmpty(t *testing.T) {
input := `[[]]
name = "Born to Run"`
testgenInvalid(t, input)
}
func TestInvalidTableEmpty(t *testing.T) {
input := `[]`
testgenInvalid(t, input)
}
func TestInvalidTableNestedBracketsClose(t *testing.T) {
input := `[a]b]
zyx = 42`
testgenInvalid(t, input)
}
func TestInvalidTableNestedBracketsOpen(t *testing.T) {
input := `[a[b]
zyx = 42`
testgenInvalid(t, input)
}
func TestInvalidTableWhitespace(t *testing.T) {
input := `[invalid key]`
testgenInvalid(t, input)
}
func TestInvalidTableWithPound(t *testing.T) {
input := `[key#group]
answer = 42`
testgenInvalid(t, input)
}
func TestInvalidTextAfterArrayEntries(t *testing.T) {
input := `array = [
"Is there life after an array separator?", No
"Entry"
@@ -213,21 +230,25 @@ func TestInvalidTextAfterArrayEntries(t *testing.T) {
}
func TestInvalidTextAfterInteger(t *testing.T) {
input := `answer = 42 the ultimate answer?`
testgenInvalid(t, input)
}
func TestInvalidTextAfterString(t *testing.T) {
input := `string = "Is there life after strings?" No.`
testgenInvalid(t, input)
}
func TestInvalidTextAfterTable(t *testing.T) {
input := `[error] this shouldn't be here`
testgenInvalid(t, input)
}
func TestInvalidTextBeforeArraySeparator(t *testing.T) {
input := `array = [
"Is there life before an array separator?" No,
"Entry"
@@ -236,6 +257,7 @@ func TestInvalidTextBeforeArraySeparator(t *testing.T) {
}
func TestInvalidTextInArray(t *testing.T) {
input := `array = [
"Entry 1",
I don't belong,
@@ -245,6 +267,7 @@ func TestInvalidTextInArray(t *testing.T) {
}
func TestValidArrayEmpty(t *testing.T) {
input := `thevoid = [[[[[]]]]]`
jsonRef := `{
"thevoid": { "type": "array", "value": [
@@ -261,6 +284,7 @@ func TestValidArrayEmpty(t *testing.T) {
}
func TestValidArrayNospaces(t *testing.T) {
input := `ints = [1,2,3]`
jsonRef := `{
"ints": {
@@ -276,6 +300,7 @@ func TestValidArrayNospaces(t *testing.T) {
}
func TestValidArraysHetergeneous(t *testing.T) {
input := `mixed = [[1, 2], ["a", "b"], [1.1, 2.1]]`
jsonRef := `{
"mixed": {
@@ -300,6 +325,7 @@ func TestValidArraysHetergeneous(t *testing.T) {
}
func TestValidArraysNested(t *testing.T) {
input := `nest = [["a"], ["b"]]`
jsonRef := `{
"nest": {
@@ -318,6 +344,7 @@ func TestValidArraysNested(t *testing.T) {
}
func TestValidArrays(t *testing.T) {
input := `ints = [1, 2, 3]
floats = [1.1, 2.1, 3.1]
strings = ["a", "b", "c"]
@@ -364,6 +391,7 @@ dates = [
}
func TestValidBool(t *testing.T) {
input := `t = true
f = false`
jsonRef := `{
@@ -374,6 +402,7 @@ f = false`
}
func TestValidCommentsEverywhere(t *testing.T) {
input := `# Top comment.
# Top comment.
# Top comment.
@@ -383,7 +412,7 @@ func TestValidCommentsEverywhere(t *testing.T) {
[group] # Comment
answer = 42 # Comment
# no-extraneous-keys-please = 999
# Inbetween comment.
# In between comment.
more = [ # Comment
# What about multiple # comments?
# Can you handle it?
@@ -414,6 +443,7 @@ more = [ # Comment
}
func TestValidDatetime(t *testing.T) {
input := `bestdayever = 1987-07-05T17:45:00Z`
jsonRef := `{
"bestdayever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"}
@@ -422,12 +452,14 @@ func TestValidDatetime(t *testing.T) {
}
func TestValidEmpty(t *testing.T) {
input := ``
jsonRef := `{}`
testgenValid(t, input, jsonRef)
}
func TestValidExample(t *testing.T) {
input := `best-day-ever = 1987-07-05T17:45:00Z
[numtheory]
@@ -451,6 +483,7 @@ perfection = [6, 28, 496]`
}
func TestValidFloat(t *testing.T) {
input := `pi = 3.14
negpi = -3.14`
jsonRef := `{
@@ -461,6 +494,7 @@ negpi = -3.14`
}
func TestValidImplicitAndExplicitAfter(t *testing.T) {
input := `[a.b.c]
answer = 42
@@ -480,6 +514,7 @@ better = 43`
}
func TestValidImplicitAndExplicitBefore(t *testing.T) {
input := `[a]
better = 43
@@ -499,6 +534,7 @@ answer = 42`
}
func TestValidImplicitGroups(t *testing.T) {
input := `[a.b.c]
answer = 42`
jsonRef := `{
@@ -514,6 +550,7 @@ answer = 42`
}
func TestValidInteger(t *testing.T) {
input := `answer = 42
neganswer = -42`
jsonRef := `{
@@ -524,6 +561,7 @@ neganswer = -42`
}
func TestValidKeyEqualsNospace(t *testing.T) {
input := `answer=42`
jsonRef := `{
"answer": {"type": "integer", "value": "42"}
@@ -532,6 +570,7 @@ func TestValidKeyEqualsNospace(t *testing.T) {
}
func TestValidKeySpace(t *testing.T) {
input := `"a b" = 1`
jsonRef := `{
"a b": {"type": "integer", "value": "1"}
@@ -540,6 +579,7 @@ func TestValidKeySpace(t *testing.T) {
}
func TestValidKeySpecialChars(t *testing.T) {
input := "\"~!@$^&*()_+-`1234567890[]|/?><.,;:'\" = 1\n"
jsonRef := "{\n" +
" \"~!@$^&*()_+-`1234567890[]|/?><.,;:'\": {\n" +
@@ -550,6 +590,7 @@ func TestValidKeySpecialChars(t *testing.T) {
}
func TestValidLongFloat(t *testing.T) {
input := `longpi = 3.141592653589793
neglongpi = -3.141592653589793`
jsonRef := `{
@@ -560,6 +601,7 @@ neglongpi = -3.141592653589793`
}
func TestValidLongInteger(t *testing.T) {
input := `answer = 9223372036854775807
neganswer = -9223372036854775808`
jsonRef := `{
@@ -570,6 +612,7 @@ neganswer = -9223372036854775808`
}
func TestValidMultilineString(t *testing.T) {
input := `multiline_empty_one = """"""
multiline_empty_two = """
"""
@@ -627,6 +670,7 @@ equivalent_three = """\
}
func TestValidRawMultilineString(t *testing.T) {
input := `oneline = '''This string has a ' quote character.'''
firstnl = '''
This string has a ' quote character.'''
@@ -654,6 +698,7 @@ in it.'''`
}
func TestValidRawString(t *testing.T) {
input := `backspace = 'This string has a \b backspace character.'
tab = 'This string has a \t tab character.'
newline = 'This string has a \n new line character.'
@@ -695,6 +740,7 @@ backslash = 'This string has a \\ backslash character.'`
}
func TestValidStringEmpty(t *testing.T) {
input := `answer = ""`
jsonRef := `{
"answer": {
@@ -706,6 +752,7 @@ func TestValidStringEmpty(t *testing.T) {
}
func TestValidStringEscapes(t *testing.T) {
input := `backspace = "This string has a \b backspace character."
tab = "This string has a \t tab character."
newline = "This string has a \n new line character."
@@ -767,6 +814,7 @@ notunicode4 = "This string does not have a unicode \\\u0075 escape."`
}
func TestValidStringSimple(t *testing.T) {
input := `answer = "You are not drinking enough whisky."`
jsonRef := `{
"answer": {
@@ -778,6 +826,7 @@ func TestValidStringSimple(t *testing.T) {
}
func TestValidStringWithPound(t *testing.T) {
input := `pound = "We see no # comments here."
poundcomment = "But there are # some comments here." # Did I # mess you up?`
jsonRef := `{
@@ -791,6 +840,7 @@ poundcomment = "But there are # some comments here." # Did I # mess you up?`
}
func TestValidTableArrayImplicit(t *testing.T) {
input := `[[albums.songs]]
name = "Glory Days"`
jsonRef := `{
@@ -804,6 +854,7 @@ name = "Glory Days"`
}
func TestValidTableArrayMany(t *testing.T) {
input := `[[people]]
first_name = "Bruce"
last_name = "Springsteen"
@@ -835,6 +886,7 @@ last_name = "Seger"`
}
func TestValidTableArrayNest(t *testing.T) {
input := `[[albums]]
name = "Born to Run"
@@ -846,7 +898,7 @@ name = "Born to Run"
[[albums]]
name = "Born in the USA"
[[albums.songs]]
name = "Glory Days"
@@ -874,6 +926,7 @@ name = "Born in the USA"
}
func TestValidTableArrayOne(t *testing.T) {
input := `[[people]]
first_name = "Bruce"
last_name = "Springsteen"`
@@ -889,6 +942,7 @@ last_name = "Springsteen"`
}
func TestValidTableEmpty(t *testing.T) {
input := `[a]`
jsonRef := `{
"a": {}
@@ -897,6 +951,7 @@ func TestValidTableEmpty(t *testing.T) {
}
func TestValidTableSubEmpty(t *testing.T) {
input := `[a]
[a.b]`
jsonRef := `{
@@ -906,6 +961,7 @@ func TestValidTableSubEmpty(t *testing.T) {
}
func TestValidTableWhitespace(t *testing.T) {
input := `["valid key"]`
jsonRef := `{
"valid key": {}
@@ -914,6 +970,7 @@ func TestValidTableWhitespace(t *testing.T) {
}
func TestValidTableWithPound(t *testing.T) {
input := `["key#group"]
answer = 42`
jsonRef := `{
@@ -925,6 +982,7 @@ answer = 42`
}
func TestValidUnicodeEscape(t *testing.T) {
input := `answer4 = "\u03B4"
answer8 = "\U000003B4"`
jsonRef := `{
@@ -935,6 +993,7 @@ answer8 = "\U000003B4"`
}
func TestValidUnicodeLiteral(t *testing.T) {
input := `answer = "δ"`
jsonRef := `{
"answer": {"type": "string", "value": "δ"}
-142
View File
@@ -1,142 +0,0 @@
package toml
import (
"fmt"
"reflect"
"time"
)
var kindToType = [reflect.String + 1]reflect.Type{
reflect.Bool: reflect.TypeOf(true),
reflect.String: reflect.TypeOf(""),
reflect.Float32: reflect.TypeOf(float64(1)),
reflect.Float64: reflect.TypeOf(float64(1)),
reflect.Int: reflect.TypeOf(int64(1)),
reflect.Int8: reflect.TypeOf(int64(1)),
reflect.Int16: reflect.TypeOf(int64(1)),
reflect.Int32: reflect.TypeOf(int64(1)),
reflect.Int64: reflect.TypeOf(int64(1)),
reflect.Uint: reflect.TypeOf(uint64(1)),
reflect.Uint8: reflect.TypeOf(uint64(1)),
reflect.Uint16: reflect.TypeOf(uint64(1)),
reflect.Uint32: reflect.TypeOf(uint64(1)),
reflect.Uint64: reflect.TypeOf(uint64(1)),
}
// typeFor returns a reflect.Type for a reflect.Kind, or nil if none is found.
// supported values:
// string, bool, int64, uint64, float64, time.Time, int, int8, int16, int32, uint, uint8, uint16, uint32, float32
func typeFor(k reflect.Kind) reflect.Type {
if k > 0 && int(k) < len(kindToType) {
return kindToType[k]
}
return nil
}
func simpleValueCoercion(object interface{}) (interface{}, error) {
switch original := object.(type) {
case string, bool, int64, uint64, float64, time.Time:
return original, nil
case int:
return int64(original), nil
case int8:
return int64(original), nil
case int16:
return int64(original), nil
case int32:
return int64(original), nil
case uint:
return uint64(original), nil
case uint8:
return uint64(original), nil
case uint16:
return uint64(original), nil
case uint32:
return uint64(original), nil
case float32:
return float64(original), nil
case fmt.Stringer:
return original.String(), nil
default:
return nil, fmt.Errorf("cannot convert type %T to Tree", object)
}
}
func sliceToTree(object interface{}) (interface{}, error) {
// arrays are a bit tricky, since they can represent either a
// collection of simple values, which is represented by one
// *tomlValue, or an array of tables, which is represented by an
// array of *Tree.
// holding the assumption that this function is called from toTree only when value.Kind() is Array or Slice
value := reflect.ValueOf(object)
insideType := value.Type().Elem()
length := value.Len()
if length > 0 {
insideType = reflect.ValueOf(value.Index(0).Interface()).Type()
}
if insideType.Kind() == reflect.Map {
// this is considered as an array of tables
tablesArray := make([]*Tree, 0, length)
for i := 0; i < length; i++ {
table := value.Index(i)
tree, err := toTree(table.Interface())
if err != nil {
return nil, err
}
tablesArray = append(tablesArray, tree.(*Tree))
}
return tablesArray, nil
}
sliceType := typeFor(insideType.Kind())
if sliceType == nil {
sliceType = insideType
}
arrayValue := reflect.MakeSlice(reflect.SliceOf(sliceType), 0, length)
for i := 0; i < length; i++ {
val := value.Index(i).Interface()
simpleValue, err := simpleValueCoercion(val)
if err != nil {
return nil, err
}
arrayValue = reflect.Append(arrayValue, reflect.ValueOf(simpleValue))
}
return &tomlValue{value: arrayValue.Interface(), position: Position{}}, nil
}
func toTree(object interface{}) (interface{}, error) {
value := reflect.ValueOf(object)
if value.Kind() == reflect.Map {
values := map[string]interface{}{}
keys := value.MapKeys()
for _, key := range keys {
if key.Kind() != reflect.String {
if _, ok := key.Interface().(string); !ok {
return nil, fmt.Errorf("map key needs to be a string, not %T (%v)", key.Interface(), key.Kind())
}
}
v := value.MapIndex(key)
newValue, err := toTree(v.Interface())
if err != nil {
return nil, err
}
values[key.String()] = newValue
}
return &Tree{values: values, position: Position{}}, nil
}
if value.Kind() == reflect.Array || value.Kind() == reflect.Slice {
return sliceToTree(object)
}
simpleValue, err := simpleValueCoercion(object)
if err != nil {
return nil, err
}
return &tomlValue{value: simpleValue, position: Position{}}, nil
}
-126
View File
@@ -1,126 +0,0 @@
package toml
import (
"strconv"
"testing"
"time"
)
type customString string
type stringer struct{}
func (s stringer) String() string {
return "stringer"
}
func validate(t *testing.T, path string, object interface{}) {
switch o := object.(type) {
case *Tree:
for key, tree := range o.values {
validate(t, path+"."+key, tree)
}
case []*Tree:
for index, tree := range o {
validate(t, path+"."+strconv.Itoa(index), tree)
}
case *tomlValue:
switch o.value.(type) {
case int64, uint64, bool, string, float64, time.Time,
[]int64, []uint64, []bool, []string, []float64, []time.Time:
default:
t.Fatalf("tomlValue at key %s containing incorrect type %T", path, o.value)
}
default:
t.Fatalf("value at key %s is of incorrect type %T", path, object)
}
t.Logf("validation ok %s as %T", path, object)
}
func validateTree(t *testing.T, tree *Tree) {
validate(t, "", tree)
}
func TestTreeCreateToTree(t *testing.T) {
data := map[string]interface{}{
"a_string": "bar",
"an_int": 42,
"time": time.Now(),
"int8": int8(2),
"int16": int16(2),
"int32": int32(2),
"uint8": uint8(2),
"uint16": uint16(2),
"uint32": uint32(2),
"float32": float32(2),
"a_bool": false,
"stringer": stringer{},
"nested": map[string]interface{}{
"foo": "bar",
},
"array": []string{"a", "b", "c"},
"array_uint": []uint{uint(1), uint(2)},
"array_table": []map[string]interface{}{{"sub_map": 52}},
"array_times": []time.Time{time.Now(), time.Now()},
"map_times": map[string]time.Time{"now": time.Now()},
"custom_string_map_key": map[customString]interface{}{customString("custom"): "custom"},
}
tree, err := TreeFromMap(data)
if err != nil {
t.Fatal("unexpected error:", err)
}
validateTree(t, tree)
}
func TestTreeCreateToTreeInvalidLeafType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": t})
expected := "cannot convert type *testing.T to Tree"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestTreeCreateToTreeInvalidMapKeyType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": map[int]interface{}{2: 1}})
expected := "map key needs to be a string, not int (int)"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestTreeCreateToTreeInvalidArrayMemberType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": []*testing.T{t}})
expected := "cannot convert type *testing.T to Tree"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestTreeCreateToTreeInvalidTableGroupType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": []map[string]interface{}{{"hello": t}}})
expected := "cannot convert type *testing.T to Tree"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestRoundTripArrayOfTables(t *testing.T) {
orig := "\n[[stuff]]\n name = \"foo\"\n things = [\"a\",\"b\"]\n"
tree, err := Load(orig)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
m := tree.ToMap()
tree, err = TreeFromMap(m)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
want := orig
got := tree.String()
if got != want {
t.Errorf("want:\n%s\ngot:\n%s", want, got)
}
}
-475
View File
@@ -1,475 +0,0 @@
package toml
import (
"bytes"
"fmt"
"io"
"math"
"math/big"
"reflect"
"sort"
"strconv"
"strings"
"time"
)
type valueComplexity int
const (
valueSimple valueComplexity = iota + 1
valueComplex
)
type sortNode struct {
key string
complexity valueComplexity
}
// Encodes a string to a TOML-compliant multi-line string value
// This function is a clone of the existing encodeTomlString function, except that whitespace characters
// are preserved. Quotation marks and backslashes are also not escaped.
func encodeMultilineTomlString(value string) string {
var b bytes.Buffer
for _, rr := range value {
switch rr {
case '\b':
b.WriteString(`\b`)
case '\t':
b.WriteString("\t")
case '\n':
b.WriteString("\n")
case '\f':
b.WriteString(`\f`)
case '\r':
b.WriteString("\r")
case '"':
b.WriteString(`"`)
case '\\':
b.WriteString(`\`)
default:
intRr := uint16(rr)
if intRr < 0x001F {
b.WriteString(fmt.Sprintf("\\u%0.4X", intRr))
} else {
b.WriteRune(rr)
}
}
}
return b.String()
}
// Encodes a string to a TOML-compliant string value
func encodeTomlString(value string) string {
var b bytes.Buffer
for _, rr := range value {
switch rr {
case '\b':
b.WriteString(`\b`)
case '\t':
b.WriteString(`\t`)
case '\n':
b.WriteString(`\n`)
case '\f':
b.WriteString(`\f`)
case '\r':
b.WriteString(`\r`)
case '"':
b.WriteString(`\"`)
case '\\':
b.WriteString(`\\`)
default:
intRr := uint16(rr)
if intRr < 0x001F {
b.WriteString(fmt.Sprintf("\\u%0.4X", intRr))
} else {
b.WriteRune(rr)
}
}
}
return b.String()
}
func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElementPerLine bool) (string, error) {
// this interface check is added to dereference the change made in the writeTo function.
// That change was made to allow this function to see formatting options.
tv, ok := v.(*tomlValue)
if ok {
v = tv.value
} else {
tv = &tomlValue{}
}
switch value := v.(type) {
case uint64:
return strconv.FormatUint(value, 10), nil
case int64:
return strconv.FormatInt(value, 10), nil
case float64:
// Default bit length is full 64
bits := 64
// Float panics if nan is used
if !math.IsNaN(value) {
// if 32 bit accuracy is enough to exactly show, use 32
_, acc := big.NewFloat(value).Float32()
if acc == big.Exact {
bits = 32
}
}
if math.Trunc(value) == value {
return strings.ToLower(strconv.FormatFloat(value, 'f', 1, bits)), nil
}
return strings.ToLower(strconv.FormatFloat(value, 'f', -1, bits)), nil
case string:
if tv.multiline {
return "\"\"\"\n" + encodeMultilineTomlString(value) + "\"\"\"", nil
}
return "\"" + encodeTomlString(value) + "\"", nil
case []byte:
b, _ := v.([]byte)
return tomlValueStringRepresentation(string(b), indent, arraysOneElementPerLine)
case bool:
if value {
return "true", nil
}
return "false", nil
case time.Time:
return value.Format(time.RFC3339), nil
case LocalDate:
return value.String(), nil
case LocalDateTime:
return value.String(), nil
case LocalTime:
return value.String(), nil
case nil:
return "", nil
}
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Slice {
var values []string
for i := 0; i < rv.Len(); i++ {
item := rv.Index(i).Interface()
itemRepr, err := tomlValueStringRepresentation(item, indent, arraysOneElementPerLine)
if err != nil {
return "", err
}
values = append(values, itemRepr)
}
if arraysOneElementPerLine && len(values) > 1 {
stringBuffer := bytes.Buffer{}
valueIndent := indent + ` ` // TODO: move that to a shared encoder state
stringBuffer.WriteString("[\n")
for _, value := range values {
stringBuffer.WriteString(valueIndent)
stringBuffer.WriteString(value)
stringBuffer.WriteString(`,`)
stringBuffer.WriteString("\n")
}
stringBuffer.WriteString(indent + "]")
return stringBuffer.String(), nil
}
return "[" + strings.Join(values, ",") + "]", nil
}
return "", fmt.Errorf("unsupported value type %T: %v", v, v)
}
func getTreeArrayLine(trees []*Tree) (line int) {
// get lowest line number that is not 0
for _, tv := range trees {
if tv.position.Line < line || line == 0 {
line = tv.position.Line
}
}
return
}
func sortByLines(t *Tree) (vals []sortNode) {
var (
line int
lines []int
tv *Tree
tom *tomlValue
node sortNode
)
vals = make([]sortNode, 0)
m := make(map[int]sortNode)
for k := range t.values {
v := t.values[k]
switch v.(type) {
case *Tree:
tv = v.(*Tree)
line = tv.position.Line
node = sortNode{key: k, complexity: valueComplex}
case []*Tree:
line = getTreeArrayLine(v.([]*Tree))
node = sortNode{key: k, complexity: valueComplex}
default:
tom = v.(*tomlValue)
line = tom.position.Line
node = sortNode{key: k, complexity: valueSimple}
}
lines = append(lines, line)
vals = append(vals, node)
m[line] = node
}
sort.Ints(lines)
for i, line := range lines {
vals[i] = m[line]
}
return vals
}
func sortAlphabetical(t *Tree) (vals []sortNode) {
var (
node sortNode
simpVals []string
compVals []string
)
vals = make([]sortNode, 0)
m := make(map[string]sortNode)
for k := range t.values {
v := t.values[k]
switch v.(type) {
case *Tree, []*Tree:
node = sortNode{key: k, complexity: valueComplex}
compVals = append(compVals, node.key)
default:
node = sortNode{key: k, complexity: valueSimple}
simpVals = append(simpVals, node.key)
}
vals = append(vals, node)
m[node.key] = node
}
// Simples first to match previous implementation
sort.Strings(simpVals)
i := 0
for _, key := range simpVals {
vals[i] = m[key]
i++
}
sort.Strings(compVals)
for _, key := range compVals {
vals[i] = m[key]
i++
}
return vals
}
func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool) (int64, error) {
return t.writeToOrdered(w, indent, keyspace, bytesCount, arraysOneElementPerLine, OrderAlphabetical)
}
func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool, ord marshalOrder) (int64, error) {
var orderedVals []sortNode
switch ord {
case OrderPreserve:
orderedVals = sortByLines(t)
default:
orderedVals = sortAlphabetical(t)
}
for _, node := range orderedVals {
switch node.complexity {
case valueComplex:
k := node.key
v := t.values[k]
combinedKey := k
if keyspace != "" {
combinedKey = keyspace + "." + combinedKey
}
var commented string
if t.commented {
commented = "# "
}
switch node := v.(type) {
// node has to be of those two types given how keys are sorted above
case *Tree:
tv, ok := t.values[k].(*Tree)
if !ok {
return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
}
if tv.comment != "" {
comment := strings.Replace(tv.comment, "\n", "\n"+indent+"#", -1)
start := "# "
if strings.HasPrefix(comment, "#") {
start = ""
}
writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment)
bytesCount += int64(writtenBytesCountComment)
if errc != nil {
return bytesCount, errc
}
}
writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[", combinedKey, "]\n")
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
}
bytesCount, err = node.writeToOrdered(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine, ord)
if err != nil {
return bytesCount, err
}
case []*Tree:
for _, subTree := range node {
writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[[", combinedKey, "]]\n")
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
}
bytesCount, err = subTree.writeToOrdered(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine, ord)
if err != nil {
return bytesCount, err
}
}
}
default: // Simple
k := node.key
v, ok := t.values[k].(*tomlValue)
if !ok {
return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
}
repr, err := tomlValueStringRepresentation(v, indent, arraysOneElementPerLine)
if err != nil {
return bytesCount, err
}
if v.comment != "" {
comment := strings.Replace(v.comment, "\n", "\n"+indent+"#", -1)
start := "# "
if strings.HasPrefix(comment, "#") {
start = ""
}
writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment, "\n")
bytesCount += int64(writtenBytesCountComment)
if errc != nil {
return bytesCount, errc
}
}
var commented string
if v.commented {
commented = "# "
}
quotedKey := quoteKeyIfNeeded(k)
writtenBytesCount, err := writeStrings(w, indent, commented, quotedKey, " = ", repr, "\n")
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
}
}
}
return bytesCount, nil
}
// quote a key if it does not fit the bare key format (A-Za-z0-9_-)
// quoted keys use the same rules as strings
func quoteKeyIfNeeded(k string) string {
// when encoding a map with the 'quoteMapKeys' option enabled, the tree will contain
// keys that have already been quoted.
// not an ideal situation, but good enough of a stop gap.
if len(k) >= 2 && k[0] == '"' && k[len(k)-1] == '"' {
return k
}
isBare := true
for _, r := range k {
if !isValidBareChar(r) {
isBare = false
break
}
}
if isBare {
return k
}
return quoteKey(k)
}
func quoteKey(k string) string {
return "\"" + encodeTomlString(k) + "\""
}
func writeStrings(w io.Writer, s ...string) (int, error) {
var n int
for i := range s {
b, err := io.WriteString(w, s[i])
n += b
if err != nil {
return n, err
}
}
return n, nil
}
// WriteTo encode the Tree as Toml and writes it to the writer w.
// Returns the number of bytes written in case of success, or an error if anything happened.
func (t *Tree) WriteTo(w io.Writer) (int64, error) {
return t.writeTo(w, "", "", 0, false)
}
// ToTomlString generates a human-readable representation of the current tree.
// Output spans multiple lines, and is suitable for ingest by a TOML parser.
// If the conversion cannot be performed, ToString returns a non-nil error.
func (t *Tree) ToTomlString() (string, error) {
b, err := t.Marshal()
if err != nil {
return "", err
}
return string(b), nil
}
// String generates a human-readable representation of the current tree.
// Alias of ToString. Present to implement the fmt.Stringer interface.
func (t *Tree) String() string {
result, _ := t.ToTomlString()
return result
}
// ToMap recursively generates a representation of the tree using Go built-in structures.
// The following types are used:
//
// * bool
// * float64
// * int64
// * string
// * uint64
// * time.Time
// * map[string]interface{} (where interface{} is any of this list)
// * []interface{} (where interface{} is any of this list)
func (t *Tree) ToMap() map[string]interface{} {
result := map[string]interface{}{}
for k, v := range t.values {
switch node := v.(type) {
case []*Tree:
var array []interface{}
for _, item := range node {
array = append(array, item.ToMap())
}
result[k] = array
case *Tree:
result[k] = node.ToMap()
case *tomlValue:
result[k] = node.value
}
}
return result
}
-400
View File
@@ -1,400 +0,0 @@
package toml
import (
"bytes"
"errors"
"fmt"
"reflect"
"strings"
"testing"
"time"
)
type failingWriter struct {
failAt int
written int
buffer bytes.Buffer
}
func (f *failingWriter) Write(p []byte) (n int, err error) {
count := len(p)
toWrite := f.failAt - (count + f.written)
if toWrite < 0 {
toWrite = 0
}
if toWrite > count {
f.written += count
f.buffer.Write(p)
return count, nil
}
f.buffer.Write(p[:toWrite])
f.written = f.failAt
return toWrite, fmt.Errorf("failingWriter failed after writing %d bytes", f.written)
}
func assertErrorString(t *testing.T, expected string, err error) {
expectedErr := errors.New(expected)
if err == nil || err.Error() != expectedErr.Error() {
t.Errorf("expecting error %s, but got %s instead", expected, err)
}
}
func TestTreeWriteToEmptyTable(t *testing.T) {
doc := `[[empty-tables]]
[[empty-tables]]`
toml, err := Load(doc)
if err != nil {
t.Fatal("Unexpected Load error:", err)
}
tomlString, err := toml.ToTomlString()
if err != nil {
t.Fatal("Unexpected ToTomlString error:", err)
}
expected := `
[[empty-tables]]
[[empty-tables]]
`
if tomlString != expected {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, tomlString)
}
}
func TestTreeWriteToTomlString(t *testing.T) {
toml, err := Load(`name = { first = "Tom", last = "Preston-Werner" }
points = { x = 1, y = 2 }`)
if err != nil {
t.Fatal("Unexpected error:", err)
}
tomlString, _ := toml.ToTomlString()
reparsedTree, err := Load(tomlString)
assertTree(t, reparsedTree, err, map[string]interface{}{
"name": map[string]interface{}{
"first": "Tom",
"last": "Preston-Werner",
},
"points": map[string]interface{}{
"x": int64(1),
"y": int64(2),
},
})
}
func TestTreeWriteToTomlStringSimple(t *testing.T) {
tree, err := Load("[foo]\n\n[[foo.bar]]\na = 42\n\n[[foo.bar]]\na = 69\n")
if err != nil {
t.Errorf("Test failed to parse: %v", err)
return
}
result, err := tree.ToTomlString()
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
expected := "\n[foo]\n\n [[foo.bar]]\n a = 42\n\n [[foo.bar]]\n a = 69\n"
if result != expected {
t.Errorf("Expected got '%s', expected '%s'", result, expected)
}
}
func TestTreeWriteToTomlStringKeysOrders(t *testing.T) {
for i := 0; i < 100; i++ {
tree, _ := Load(`
foobar = true
bar = "baz"
foo = 1
[qux]
foo = 1
bar = "baz2"`)
stringRepr, _ := tree.ToTomlString()
t.Log("Intermediate string representation:")
t.Log(stringRepr)
r := strings.NewReader(stringRepr)
toml, err := LoadReader(r)
if err != nil {
t.Fatal("Unexpected error:", err)
}
assertTree(t, toml, err, map[string]interface{}{
"foobar": true,
"bar": "baz",
"foo": 1,
"qux": map[string]interface{}{
"foo": 1,
"bar": "baz2",
},
})
}
}
func testMaps(t *testing.T, actual, expected map[string]interface{}) {
if !reflect.DeepEqual(actual, expected) {
t.Fatal("trees aren't equal.\n", "Expected:\n", expected, "\nActual:\n", actual)
}
}
func TestTreeWriteToMapSimple(t *testing.T) {
tree, _ := Load("a = 42\nb = 17")
expected := map[string]interface{}{
"a": int64(42),
"b": int64(17),
}
testMaps(t, tree.ToMap(), expected)
}
func TestTreeWriteToInvalidTreeSimpleValue(t *testing.T) {
tree := Tree{values: map[string]interface{}{"foo": int8(1)}}
_, err := tree.ToTomlString()
assertErrorString(t, "invalid value type at foo: int8", err)
}
func TestTreeWriteToInvalidTreeTomlValue(t *testing.T) {
tree := Tree{values: map[string]interface{}{"foo": &tomlValue{value: int8(1), comment: "", position: Position{}}}}
_, err := tree.ToTomlString()
assertErrorString(t, "unsupported value type int8: 1", err)
}
func TestTreeWriteToInvalidTreeTomlValueArray(t *testing.T) {
tree := Tree{values: map[string]interface{}{"foo": &tomlValue{value: int8(1), comment: "", position: Position{}}}}
_, err := tree.ToTomlString()
assertErrorString(t, "unsupported value type int8: 1", err)
}
func TestTreeWriteToFailingWriterInSimpleValue(t *testing.T) {
toml, _ := Load(`a = 2`)
writer := failingWriter{failAt: 0, written: 0}
_, err := toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writing 0 bytes", err)
}
func TestTreeWriteToFailingWriterInTable(t *testing.T) {
toml, _ := Load(`
[b]
a = 2`)
writer := failingWriter{failAt: 2, written: 0}
_, err := toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writing 2 bytes", err)
writer = failingWriter{failAt: 13, written: 0}
_, err = toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writing 13 bytes", err)
}
func TestTreeWriteToFailingWriterInArray(t *testing.T) {
toml, _ := Load(`
[[b]]
a = 2`)
writer := failingWriter{failAt: 2, written: 0}
_, err := toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writing 2 bytes", err)
writer = failingWriter{failAt: 15, written: 0}
_, err = toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writing 15 bytes", err)
}
func TestTreeWriteToMapExampleFile(t *testing.T) {
tree, _ := LoadFile("example.toml")
expected := map[string]interface{}{
"title": "TOML Example",
"owner": map[string]interface{}{
"name": "Tom Preston-Werner",
"organization": "GitHub",
"bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
"dob": time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
},
"database": map[string]interface{}{
"server": "192.168.1.1",
"ports": []interface{}{int64(8001), int64(8001), int64(8002)},
"connection_max": int64(5000),
"enabled": true,
},
"servers": map[string]interface{}{
"alpha": map[string]interface{}{
"ip": "10.0.0.1",
"dc": "eqdc10",
},
"beta": map[string]interface{}{
"ip": "10.0.0.2",
"dc": "eqdc10",
},
},
"clients": map[string]interface{}{
"data": []interface{}{
[]interface{}{"gamma", "delta"},
[]interface{}{int64(1), int64(2)},
},
},
}
testMaps(t, tree.ToMap(), expected)
}
func TestTreeWriteToMapWithTablesInMultipleChunks(t *testing.T) {
tree, _ := Load(`
[[menu.main]]
a = "menu 1"
b = "menu 2"
[[menu.main]]
c = "menu 3"
d = "menu 4"`)
expected := map[string]interface{}{
"menu": map[string]interface{}{
"main": []interface{}{
map[string]interface{}{"a": "menu 1", "b": "menu 2"},
map[string]interface{}{"c": "menu 3", "d": "menu 4"},
},
},
}
treeMap := tree.ToMap()
testMaps(t, treeMap, expected)
}
func TestTreeWriteToMapWithArrayOfInlineTables(t *testing.T) {
tree, _ := Load(`
[params]
language_tabs = [
{ key = "shell", name = "Shell" },
{ key = "ruby", name = "Ruby" },
{ key = "python", name = "Python" }
]`)
expected := map[string]interface{}{
"params": map[string]interface{}{
"language_tabs": []interface{}{
map[string]interface{}{
"key": "shell",
"name": "Shell",
},
map[string]interface{}{
"key": "ruby",
"name": "Ruby",
},
map[string]interface{}{
"key": "python",
"name": "Python",
},
},
},
}
treeMap := tree.ToMap()
testMaps(t, treeMap, expected)
}
func TestTreeWriteToFloat(t *testing.T) {
tree, err := Load(`a = 3.0`)
if err != nil {
t.Fatal(err)
}
str, err := tree.ToTomlString()
if err != nil {
t.Fatal(err)
}
expected := `a = 3.0`
if strings.TrimSpace(str) != strings.TrimSpace(expected) {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, str)
}
}
func TestTreeWriteToSpecialFloat(t *testing.T) {
expected := `a = +inf
b = -inf
c = nan`
tree, err := Load(expected)
if err != nil {
t.Fatal(err)
}
str, err := tree.ToTomlString()
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(str) != strings.TrimSpace(expected) {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, str)
}
}
func TestIssue290(t *testing.T) {
tomlString :=
`[table]
"127.0.0.1" = "value"
"127.0.0.1:8028" = "value"
"character encoding" = "value"
"ʎǝʞ" = "value"`
t1, err := Load(tomlString)
if err != nil {
t.Fatal("load err:", err)
}
s, err := t1.ToTomlString()
if err != nil {
t.Fatal("ToTomlString err:", err)
}
_, err = Load(s)
if err != nil {
t.Fatal("reload err:", err)
}
}
func BenchmarkTreeToTomlString(b *testing.B) {
toml, err := Load(sampleHard)
if err != nil {
b.Fatal("Unexpected error:", err)
}
for i := 0; i < b.N; i++ {
_, err := toml.ToTomlString()
if err != nil {
b.Fatal(err)
}
}
}
var sampleHard = `# Test file for TOML
# Only this one tries to emulate a TOML file written by a user of the kind of parser writers probably hate
# This part you'll really hate
[the]
test_string = "You'll hate me after this - #" # " Annoying, isn't it?
[the.hard]
test_array = [ "] ", " # "] # ] There you go, parse this!
test_array2 = [ "Test #11 ]proved that", "Experiment #9 was a success" ]
# You didn't think it'd as easy as chucking out the last #, did you?
another_test_string = " Same thing, but with a string #"
harder_test_string = " And when \"'s are in the string, along with # \"" # "and comments are there too"
# Things will get harder
[the.hard."bit#"]
"what?" = "You don't think some user won't do that?"
multi_line_array = [
"]",
# ] Oh yes I did
]
# Each of the following keygroups/key value pairs should produce an error. Uncomment to them to test
#[error] if you didn't catch this, your parser is broken
#string = "Anything other than tabs, spaces and newline after a keygroup or key value pair has ended should produce an error unless it is a comment" like this
#array = [
# "This might most likely happen in multiline arrays",
# Like here,
# "or here,
# and here"
# ] End of array comment, forgot the #
#number = 3.14 pi <--again forgot the # `
+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{}{})
+1099
View File
File diff suppressed because it is too large Load Diff
+2181
View File
File diff suppressed because it is too large Load Diff