Compare commits

..

182 Commits

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

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

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

Those should now start in discussions

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

* Update lincese year

* ci: do not run coverage on master

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

Fixes #440.

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

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

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

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

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

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

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

* Remove fuzzit

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

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

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

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

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

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

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

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

Benchmarks were run from the benchmark/ directory using:

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

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

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

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

Fields in anonymous structs are not supported at this point.

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

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

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

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

Unmarshalling code handles this scenario gracefully.

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

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

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

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

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

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

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

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

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-20 21:21:26 -05:00
dependabot-preview[bot] c4efb7477c Bump gopkg.in/yaml.v2 from 2.2.4 to 2.2.5 (#322) 2019-11-06 15:49:39 +00:00
Thomas Pelletier 903d9455db TOML 0.5.0 (#320)
go-toml now officially supports all TOML 0.5.0 features. If anything
does not work according to the spec, please file a bug!
2019-10-25 14:53:56 -04:00
Thomas Pelletier a89a075e1b Test for accidental newlines (#319) 2019-10-25 14:44:53 -04:00
Thomas Pelletier 5e74bb91ea Local time support (#318) 2019-10-25 14:28:32 -04:00
Thomas Pelletier 3a4d7af89e Local DateTime support (#317) 2019-10-25 14:07:46 -04:00
Thomas Pelletier 8a362ad712 Short-date support (#298) 2019-10-25 13:21:44 -04:00
Thomas Pelletier 5edf9acd3e Add testing for encodeMultilineTomlString (#313) 2019-10-21 14:31:28 -04:00
Thomas Pelletier e95df67ba3 Delete token.Int() (#312)
Not used anywhere.
2019-10-21 14:01:10 -04:00
Jonathan Lloyd bef0f57967 Fix key parsing in line tables (#311)
A bug was reported that indicated that inline tables did not fully support bare keys:
$ echo 'foo = { -bar => "buz"}' | ./tomljson
(1, 9): unexpected token type in inline table: Error

$ echo 'foo = { "whatever" = "buz"}' | ./tomljson
(1, 10): unexpected token type in inline table: String

echo 'foo = { _no = "buz"}' | ./tomljson
(1, 9): unexpected token type in inline table: Error

This change makes a couple of tweaks to to allow for all key variants in inline tables

Fixes: #282
2019-10-20 20:36:14 -04:00
Marcin Białoń e87c92d4f4 Marshal arrays (#310)
Fixes #285
2019-10-09 12:33:56 -04:00
dependabot-preview[bot] 8fe62057ea Bump gopkg.in/yaml.v2 from 2.2.3 to 2.2.4 (#309)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.3 to 2.2.4.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.3...v2.2.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-05 15:18:21 -04:00
dependabot-preview[bot] 5f42261979 Bump gopkg.in/yaml.v2 from 2.2.2 to 2.2.3 (#308)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.2 to 2.2.3.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.2...v2.2.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-01 06:57:01 -04:00
Marcin Białoń 75654e60b8 Tree.Marshal returns the TOML encoding of Tree (#306)
The Tree.Marshal tried to marshal the Tree struct itself rather than the nodes being part of the tree.

Fixes #295
2019-09-26 13:39:15 -07:00
Thomas Pelletier 091e2dc498 Set up CI with Azure Pipelines (#304) 2019-09-24 16:11:17 -07:00
Marcin Białoń 095a905e04 Allow space to separate date and time (#300)
Fixes #231
2019-09-19 10:45:53 -07:00
Thomas Pelletier ec312409d3 Update fuzzit.dev script to latest (#301)
Fixes #299
2019-09-18 09:04:23 -07:00
Thomas Pelletier 26fd12ff54 Fix fuzzit master (#297) 2019-09-09 20:59:59 -07:00
Thomas Pelletier b40204d36a Replace CIs by Github Actions (#294) 2019-09-09 19:44:45 -07:00
Gaurav Dhameeja 4d5afd743f jsontoml tool (#296)
`jsontoml` is very similar to `tomljson`
It uses json.Unmarshal to convert read json to map and then
converts the map to tree using `toml.TreeFromMap`.
Then this tree is converted to toml using `tree.toTomlString()`
The numbers when taken as input from json get converted to float64
because of how `json.Unmarshal()` converts all json numbers to float.

Fixes #280
2019-09-06 09:36:56 -07:00
Chris 3ded2e09ee Fix float64 truncation error (#293)
Don't truncate float64 representation on marashaling.

Fixes https://github.com/pelletier/go-toml/issues/289
2019-08-26 20:57:02 -07:00
Yevgeny Pats 781fbae71e Add fuzzit.dev continuous fuzzing integration (#288) 2019-08-19 12:53:00 -07:00
Thomas Pelletier 68063a447e Quote keys during encoding when the key isn't bare (#291)
In case the key contains non-bare characters (out of `A-Za-z0-9_-`), the
key needs to be quoted during encoding to be valid TOML.
2019-08-18 23:00:12 -07:00
Roberth Kulbin 84da2c4a25 Merge struct fields in Unmarshal (#284)
* add test for unexported field preservation
* merge struct values instead of replacing them
* use struct merging on nested value structs
* unmarshalling merges nested struct pointers when non-nil
2019-07-25 00:06:17 -07:00
Kamil Samigullin dba45d427f Handle anonymous structs (#281)
Handle anonymous structs during Unmarshal.

Fixes #279
2019-05-29 20:55:49 -07:00
Gregory Oschwald 728039f679 Handle other key types in Unmarshal (#276)
Previously, this would fail with:

```
panic: reflect.Value.SetMapIndex: value of type string is not assignable to type toml.letter [recovered]
panic: reflect.Value.SetMapIndex: value of type string is not assignable to type toml.letter
```

Now this only panics when the key type cannot be converted from a
string.
2019-04-29 20:50:10 -07:00
Gregory Oschwald 1d8903f1d0 Allow unmarshaling to top level maps (#273) 2019-04-24 23:15:40 -07:00
Brent DeSpain 65b27e6823 Order map keys alphabetically (#270)
This makes sure we have a stable output when marshaling
maps.

Fixes #268
2019-04-11 13:52:29 +01:00
Thomas Pelletier 6ea91ef590 Do not push Docker images for forked repositories (#272)
For security reasons, CircleCI does not make environment variables
available on forked repositories (often used in PRs). This will still
build the docker image, but won't try to push it to dockerhub.
2019-04-11 13:49:07 +01:00
Ceriath 51edd0ca49 Fix goreportcard issues (#271)
* Fixed misspell

* Fixed ineffassign

`user` and `password` always got overwritten
`orderedVals` was initialized with an empty array but always got overwritten by either `sortByLines()` or `sortAlphabetical`
`err` was assigned a `nil` value that was either overwritten or unused anyways

* Fix comment for DeletePath

The comment assumed the method was named Delete, i guess a rename happened at some point

* Update doc_test.go
2019-04-11 12:11:29 +01:00
Thomas Pelletier d95bfe020e Dockerfile (#269)
Provide docker images for go-toml tools.

Ref: https://github.com/pelletier/go-toml/pull/267
2019-04-10 13:43:12 +01:00
Brent DeSpain 63909f0a90 Option to keep fields ordered when marshal struct (#266)
Adds a new `Order()` option to preserve order of struct fields when
marshaling.
2019-04-02 09:47:51 -07:00
Thomas Pelletier f9070d3b40 Use go mod (#265) 2019-03-21 17:22:05 -07:00
Thomas Pelletier 405d48dc28 Port toml-test to pure Go (#264)
* Port toml-test to pure Go

This change basically ports the toml-test examples test suite to pure
Go. This removes the snowflake test.sh required to run such tests, and
allows us to the example tests on any platform (which includes Windows
as part of the pull-request testing).

* Allow CircleCI failure for go tip
2019-03-20 00:23:14 -07:00
Thomas Pelletier 690ec00a4b Circleci (#262)
Implement CircleCI as an alternative for Travis.
2019-03-12 21:57:14 -07:00
Thomas Pelletier bef2d19cb0 Go 1.12 (#261) 2019-03-05 20:19:32 -08:00
Thomas Pelletier e1803f96f6 Support dotted-keys (#260)
Implement dotted keys as sequence of bare and quoted keys. Introduced in
TOML 0.5.0.
Fixes #230
2019-03-04 22:35:03 -08:00
Thomas Pelletier d9a27b8052 Provide "default" tag for unmarshal (#259)
When a struct is unmarshalled, go-toml now looks at the `default` tag to
provide a default value in case the key is not present in the TOML
document. This is only implemented for string, bool, int, int64,
float64. Additional types can be further implemented on a request-basis.
2019-03-01 17:18:23 -08:00
David Poncelow ad2aec1dcc Delete function to Tree (#256)
Adds delete* functions to the tree so that keys can be removed programatically.
2019-03-01 14:25:52 -08:00
Thomas Pelletier 489c49b1b4 Add CONTRIBUTING document (#258)
Fixes #180
2019-03-01 13:26:01 -08:00
Thomas Pelletier 27c6b39a13 Fossa badge 2018-11-23 16:27:27 -08:00
Thomas Pelletier 539dd095b3 Support byte order mark (#253)
Fixes #250
2018-11-23 19:15:58 -05:00
Thomas Pelletier b56e1b27b4 Update Go version in Appveyor (#246) 2018-11-19 11:27:10 -05:00
Andriy Senyshyn 19cbd226da Allow to marshal pointer to struct and map (#247) 2018-11-19 10:31:15 -05:00
Tom Wambold 0a1666a81f Map camelCased keys to fields in structs (#251)
The name for each field in a struct is used to look up a key in the TOML
tree.  A few different (case-sensitive) forms of this name are tried.
Previously, the current, lower-cased, and title-cased versions of the
name are tried.  This precludes camelCased keys from mapping back to
fields in structs.  This change adds camelCase to the set of keys to
try.

For example, the following TOML:

  fooBar = 10

Would previously *not* map to the following struct:

  type Foo struct {
    FooBar int
  }

This change corrects this.
2018-11-19 10:29:38 -05:00
Andriy Senyshyn aa79e12a97 Support time.duration (#248) 2018-11-12 09:02:56 -08:00
Veselkov Konstantin 81a861c69d Fix typeSwitchVar warnings (#243)
Fixes #242
2018-09-30 13:58:32 -07:00
xiehuc 78b76feda6 Fix integer keys in inline tables (#239)
Fixes #224
2018-09-22 11:02:51 -07:00
Andriy Senyshyn 90d6f96e9e Allow to change default tags for Decoder and Encoder (#241)
Decoder: allow to customize default field name tag "toml" on decoding.
Example:
```
type doc struct {
    title `file:"header"`
}
```

Encoder: allow to customize tags for encoding struct to toml.
Example:
```
type doc struct {
    title `file:"header" description:"document title"`
}
```

Fixes #238
2018-09-21 09:41:11 -07:00
Jayi e33f654429 fix panic when type unmatch between toml and struct (#236) 2018-09-17 21:16:20 -07:00
Karthik K 4edab6691b Travis check for golang 1.11 (#240) 2018-09-17 21:05:06 -07:00
Thomas Pelletier c2dbbc24a9 Add Codecov badge to README 2018-07-24 11:51:02 -07:00
Thomas Pelletier 14d3ac30da Setup Codecov (#235) 2018-07-24 11:27:17 -07:00
Thomas Pelletier 5c5490133d Create PULL_REQUEST_TEMPLATE.md 2018-07-18 17:16:04 -07:00
Thomas Pelletier 216c9ec838 Update issue templates 2018-07-18 17:08:05 -07:00
Thomas Pelletier a295f02a64 AppVeyor Windows build (#234)
Fixes #229
2018-07-18 16:44:55 -07:00
Thomas Pelletier dbe63ccdd0 Pin toml-test version (#233)
Their latest master has tests for features of TOML 0.5.0 that are not
yet supported by go-toml.

Fixes #228.
2018-07-18 16:15:26 -07:00
Yang Luo 603baefff9 Fix path not found message on Windows (#227) 2018-07-03 11:33:37 -07:00
Alan Murtagh c01d1270ff Multiline Marshal tag (#221)
The new multiline tag works just like the existing 'commented' tag (i.e.
`multiline:"true"`), and tells go-toml to marshal the value as a
multi-line string. The tag currently has no impact on any non-string
fields.
2018-06-05 13:47:19 -07:00
Cameron Moore 66540cf1fc Go 1.10 support (#223)
* Update Travis CI to use latest Go releases

* Fix go vet issues for Go 1.10

Starting in Go 1.10, the `go test` command now automatically runs `go
vet`. This commit fixes two issues flagged by vet that cause `go test`
to fail.

* Fix gofmt issue for Go 1.10

Go 1.10 introduced a small formatting change with comments in empty
multiline slice definitions.  This commit attempts to move the offending
comment in such a way that all version of gofmt will agree on its
location.

* Remove go-vet from test.sh

Starting in Go 1.10, the `go test` command automatically runs `go vet`,
so we don't need to run `go vet` explicitly from within test.sh.

Fixes #222
2018-03-23 11:52:43 -07:00
Chris 05bcc0fb0d Make multi-line arrays always use trailing commas (#217)
This makes ArraysWithOneElementPerLine output arrays with commas after every element.

```
A = [1,2,3]
```

Now becomes:

```
A = [
  1,
  2,
  3,
]
```
2018-02-28 15:36:31 -08:00
Thomas Pelletier acdc450948 Fix backward incompatibility for Set* methods (#213)
Patch #185 introduced a backward incompatibility by changing the arguments
of the `Set*` methods on `Tree`.

This change restores the arguments to what they previous were, and
introduces `SetWithComment` and `SetPathWithComment` to perform the same
action.
2018-01-18 14:54:55 -08:00
Jelte Fennema 778c285afa Add support for special float values (inf and nan) (#210) 2018-01-18 14:10:55 -08:00
Jelte Fennema a1e8a8d702 Unmarshal into custom types and error on overflows (#209)
This fixes two unmarshalling issues:

1. Unmarshalling into a custom integer/float type (e.g. `time.Duration`).
2. Checks for overflows happen before unmarshalling, erroring if an overflow
would happen.

Apart from this it also reduces code duplication a bit.
2018-01-18 14:08:34 -08:00
Jelte Fennema 03c6bf4172 Actually show the error message from an Error token (#208) 2018-01-18 14:03:34 -08:00
Thomas Pelletier a1b12e18b7 Fix false positive when running test.sh (#212)
Patch #193 introduced a regression in the toml-tests examples, but it was
never caught because test.sh was exiting with a zero status code, even
though the tests failed. This is because of the `|tee` operation when
invoking toml-test, without setting the pipefail option, reporting the
status code of `tee` instead of `toml-test`.
2018-01-18 14:02:09 -08:00
Kazuyoshi Kato 4874e8477b Fix parsing of single quoted keys (#201)
Patch #193 doesn't work correctly because that must be handled by the
lexer, and `parseKey()` must not handle escape sequences.

Ref #61
2018-01-18 13:52:12 -08:00
Thomas Pelletier 9bf0212445 Bump go versions (#211) 2018-01-15 16:08:35 -08:00
Thomas Pelletier 0131db6d73 Lint (#206) 2017-12-22 12:45:48 +01:00
Thomas Pelletier 861c4734ac Support for hex, oct, and bin integers (#205)
Add support for non-decimal integers. At the time of writing, this is an
unreleased backward-compatible feature of TOML:

```
  Non-negative integer values may also be expressed in hexadecimal, octal, or
  binary. In these formats, leading zeros are allowed (after the prefix). Hex
  values are case insensitive. Underscores are allowed between digits (but
  not between the prefix and the value).

  # hexadecimal with prefix `0x`
  hex1 = 0xDEADBEEF
  hex2 = 0xdeadbeef
  hex3 = 0xdead_beef

  # octal with prefix `0o`
  oct1 = 0o01234567
  oct2 = 0o755 # useful for Unix file permissions

  # binary with prefix `0b`
  bin1 = 0b11010110
```

Fixes #204
2017-12-22 12:24:26 +01:00
Thomas Pelletier b8b5e76965 Add Encoder opt to emit arrays on multiple lines (#203)
A new Encoder option emits arrays with more than one line on multiple lines.
This is off by default and toggled with `ArraysWithOneElementPerLine`.

For example:

```
A = [1,2,3]
```

Becomes:

```
A = [
  1,
  2,
  3
]
```

Fixes #200
2017-12-18 14:57:16 +01:00
Robert Günzler 4e9e0ee19b Add Encoder/Decoder types (#192)
Usage is similar to the stdlibs JSON encoder/decoder but I tried to
leave the general structure of the code the same.

Main motivation was to support encoding/decoding options to allow
encoding string-type map keys as quoted TOML keys.
This was implemented on the Encoder with QuoteMapKeys(bool).

> The TOML spec supports using UTF-8 strings as keys.
> https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md#table
2017-10-24 14:10:38 -07:00
Thomas Pelletier 8c31c2ec65 Fix fuzz (#199)
Fix fuzzer (LoadString does not exist), and mention it in the README.
2017-10-21 19:23:38 -07:00
Thomas Pelletier 6d858869d3 Describe versioning policy in README (#198) 2017-10-21 19:08:12 -07:00
Kazuyoshi Kato 1916042ba2 Add fuzz.sh to do fuzzing with go-fuzz (#194)
Fixes #181
2017-10-21 16:37:53 -07:00
Kazuyoshi Kato a410399d2c Support single quoted keys (#193)
Fixes #61
2017-10-21 23:14:36 +00:00
Kazuyoshi Kato 878c11e70e Unmarshal should report a type mismatch as an error (#196)
Fixes #186
2017-10-21 15:29:03 -07:00
Kazuyoshi Kato 19ece5dc77 Fix typos (#195)
Most of them are caught by Go Report Card.
https://goreportcard.com/report/github.com/pelletier/go-toml
2017-10-21 15:26:06 -07:00
Maxime Le Conte des Floris d01db88be9 Fix example code in README (#197) 2017-10-21 15:24:36 -07:00
Thomas Pelletier 2009e44b6f Improve doc for un/marshal functions (#189) 2017-10-01 15:47:47 -07:00
Yvonnick Esnault 690dbc9ee7 Comment annotation for Marshal (#185) 2017-10-01 15:05:24 -07:00
KANEKO Tatsuya 16398bac15 Fix example code in README (#187) 2017-09-24 11:42:18 -07:00
Edward Betts 1d6b12b7cb Correct query/parser spelling mistake (#182) 2017-09-04 12:58:09 -07:00
Thomas Pelletier 9c1b4e331f Bump golang to 1.9 (#179) 2017-08-24 20:46:50 -07:00
178inaba 4692b8f9ba Fix Marshal examples (#178)
* Fix Marshal examples
* Move ExampleMarshal to doc test
2017-08-16 17:06:23 -07:00
Thomas Pelletier 69d355db53 Lex performance improvement (#176)
* Use []token instead of chan token

name             old time/op    new time/op    delta
ParseToml-8        1.18ms ± 0%    0.91ms ± 0%  -22.98%
UnmarshalToml-8    1.29ms ± 0%    0.95ms ± 0%  -25.96%

name             old alloc/op   new alloc/op   delta
ParseToml-8         429kB ± 0%     444kB ± 0%   +3.49%
UnmarshalToml-8     451kB ± 0%     466kB ± 0%   +3.32%

name             old allocs/op  new allocs/op  delta
ParseToml-8         14.1k ± 0%     13.7k ± 0%   -2.31%
UnmarshalToml-8     15.1k ± 0%     14.7k ± 0%   -2.16%

* Lex on []byte instead of io.Reader

name             old time/op    new time/op    delta
ParseToml-8        1.18ms ± 0%    0.29ms ± 0%  -75.18%
UnmarshalToml-8    1.27ms ± 0%    0.38ms ± 0%  -70.38%

name             old alloc/op   new alloc/op   delta
ParseToml-8         429kB ± 0%     135kB ± 0%  -68.53%
UnmarshalToml-8     451kB ± 0%     157kB ± 0%  -65.22%

name             old allocs/op  new allocs/op  delta
ParseToml-8         14.1k ± 0%      3.2k ± 0%  -77.20%
UnmarshalToml-8     15.1k ± 0%      4.2k ± 0%  -72.00%
2017-06-27 18:26:37 -07:00
Jordan Krage ef23ce9e92 WriteTo string concat allocation reduction (#177)
* reduce string concat allocs in Tree.writeTo
* fix failingWriter and usages
2017-06-27 18:24:37 -07:00
Thomas Pelletier 4a000a21a4 Benchmark against other libraries (#175)
BenchmarkUnmarshalToml-8             1000  1320117 ns/op  450932 B/op  15072 allocs/op
BenchmarkUnmarshalBurntSushiToml-8   3000   402897 ns/op   82900 B/op   1761 allocs/op
BenchmarkUnmarshalJson-8            20000    66092 ns/op    3536 B/op    101 allocs/op
BenchmarkUnmarshalYaml-8            10000   189600 ns/op   44872 B/op   1058 allocs/op
2017-06-25 13:05:13 -07:00
Thomas Pelletier fe7536c3de Run benchmarks and fmt in travis (#174) 2017-06-01 23:55:32 -07:00
Thomas Pelletier e94d595cd4 Update tested Go versions (#173) 2017-06-01 21:42:09 -07:00
Thomas Pelletier 0d5a6db8dd Fix toString float encoding (#172)
Ensure a round float does contain a decimal point. Otherwise
feeding the output back to the parser would convert to an integer.

Fixes #171
2017-06-01 21:36:58 -07:00
Cameron Moore a60c71373e Optimize some string handling (#170)
* Don't use fmt.Sprintf on simple strings
* Use bytes.Buffer in encodeTomlString

name                old time/op    new time/op    delta
Lexer-8                162µs ± 0%     161µs ± 0%   -0.12%
TreeToTomlString-8    19.7µs ± 0%     7.5µs ± 0%  -61.80%

name                old alloc/op   new alloc/op   delta
TreeToTomlString-8    9.75kB ± 0%    4.96kB ± 0%  -49.12%

name                old allocs/op  new allocs/op  delta
TreeToTomlString-8       485 ± 0%        78 ± 0%  -83.92%
2017-06-01 21:03:55 -07:00
69 changed files with 11866 additions and 1201 deletions
+2
View File
@@ -0,0 +1,2 @@
cmd/tomll/tomll
cmd/tomljson/tomljson
+31
View File
@@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us improve
---
<!--
‼️ Main development focus is on the upcoming go-toml v2 ⚠️
As a result, v1.x bugs will likely not see a fix on a v1.x version.
However, reporting the bug is the best way to ensure that it will be fixed in v2.
See https://github.com/pelletier/go-toml/discussions/506.
-->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior. Including TOML files.
**Expected behavior**
A clear and concise description of what you expected to happen, if other than "should work".
**Versions**
- go-toml: version (or git sha)
- go: version
- operating system: e.g. macOS, Windows, Linux
**Additional context**
Add any other context about the problem here that you think may help to diagnose.
+8
View File
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: daily
time: "13:00"
open-pull-requests-limit: 10
+67
View File
@@ -0,0 +1,67 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '26 19 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+4
View File
@@ -1 +1,5 @@
test_program/test_program_bin test_program/test_program_bin
fuzz/
cmd/tomll/tomll
cmd/tomljson/tomljson
cmd/tomltestgen/tomltestgen
-21
View File
@@ -1,21 +0,0 @@
sudo: false
language: go
go:
- 1.6.4
- 1.7.5
- 1.8
- tip
matrix:
allow_failures:
- go: tip
fast_finish: true
script:
- ./test.sh
before_install:
- go get github.com/axw/gocov/gocov
- go get github.com/mattn/goveralls
- if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
branches:
only: [master]
after_success:
- $HOME/gopath/bin/goveralls -service=travis-ci -coverprofile=coverage.out -repotoken $COVERALLS_TOKEN
+132
View File
@@ -0,0 +1,132 @@
## 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.
As the single maintainer of go-toml, time is scarce. All help, big or
small, is more than welcomed!
### 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.
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!
### Improve the documentation
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!
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.
### Report a bug
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
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.
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
The fairly standard code contribution process looks like that:
1. [Fork the project][fork].
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!
Feel free to ask for help! You can create draft pull requests to gather
some early feedback!
#### 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).
#### 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
#### Merge pull request
Checklist:
* Passing CI.
* Does not introduce backward-incompatible changes (unless discussed).
* Has relevant doc changes.
* Has relevant unit tests.
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
1. Go to [releases][releases]. Click on "X commits to master since this
release".
2. Make note of all the changes. Look for backward incompatible changes,
new features, and bug fixes.
3. Pick the new version using the above and semver.
4. Create a [new release][new-release].
5. Follow the same format as [1.1.0][release-110].
[issues-tracker]: https://github.com/pelletier/go-toml/issues
[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md
[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
[releases]: https://github.com/pelletier/go-toml/releases
[new-release]: https://github.com/pelletier/go-toml/releases/new
[release-110]: https://github.com/pelletier/go-toml/releases/tag/v1.1.0
+11
View File
@@ -0,0 +1,11 @@
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) The MIT License (MIT)
Copyright (c) 2013 - 2017 Thomas Pelletier, Eric Anderton Copyright (c) 2013 - 2021 Thomas Pelletier, Eric Anderton
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+29
View File
@@ -0,0 +1,29 @@
export CGO_ENABLED=0
go := go
go.goos ?= $(shell echo `go version`|cut -f4 -d ' '|cut -d '/' -f1)
go.goarch ?= $(shell echo `go version`|cut -f4 -d ' '|cut -d '/' -f2)
out.tools := tomll tomljson jsontoml
out.dist := $(out.tools:=_$(go.goos)_$(go.goarch).tar.xz)
sources := $(wildcard **/*.go)
.PHONY:
tools: $(out.tools)
$(out.tools): $(sources)
GOOS=$(go.goos) GOARCH=$(go.goarch) $(go) build ./cmd/$@
.PHONY:
dist: $(out.dist)
$(out.dist):%_$(go.goos)_$(go.goarch).tar.xz: %
if [ "$(go.goos)" = "windows" ]; then \
tar -cJf $@ $^.exe; \
else \
tar -cJf $@ $^; \
fi
.PHONY:
clean:
rm -rf $(out.tools) $(out.dist)
+5
View File
@@ -0,0 +1,5 @@
**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).
+54 -21
View File
@@ -1,15 +1,16 @@
# go-toml # go-toml
Go library for the [TOML](https://github.com/mojombo/toml) format. Go library for the [TOML](https://toml.io/) format.
This library supports TOML version This library supports TOML version
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md) [v1.0.0-rc.3](https://toml.io/en/v1.0.0-rc.3)
[![GoDoc](https://godoc.org/github.com/pelletier/go-toml?status.svg)](http://godoc.org/github.com/pelletier/go-toml) [![Go Reference](https://pkg.go.dev/badge/github.com/pelletier/go-toml.svg)](https://pkg.go.dev/github.com/pelletier/go-toml)
[![license](https://img.shields.io/github/license/pelletier/go-toml.svg)](https://github.com/pelletier/go-toml/blob/master/LICENSE) [![license](https://img.shields.io/github/license/pelletier/go-toml.svg)](https://github.com/pelletier/go-toml/blob/master/LICENSE)
[![Build Status](https://travis-ci.org/pelletier/go-toml.svg?branch=master)](https://travis-ci.org/pelletier/go-toml) [![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)
[![Coverage Status](https://coveralls.io/repos/github/pelletier/go-toml/badge.svg?branch=master)](https://coveralls.io/github/pelletier/go-toml?branch=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) [![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)
## Features ## Features
@@ -17,7 +18,7 @@ Go-toml provides the following features for using data parsed from TOML document
* Load TOML documents from files and string data * Load TOML documents from files and string data
* Easily navigate TOML structure using Tree * Easily navigate TOML structure using Tree
* Mashaling and unmarshaling to and from data structures * Marshaling and unmarshaling to and from data structures
* Line & column position data for all parsed elements * Line & column position data for all parsed elements
* [Query support similar to JSON-Path](query/) * [Query support similar to JSON-Path](query/)
* Syntax errors contain line and column numbers * Syntax errors contain line and column numbers
@@ -33,7 +34,7 @@ import "github.com/pelletier/go-toml"
Read a TOML document: Read a TOML document:
```go ```go
config, _ := toml.LoadString(` config, _ := toml.Load(`
[postgres] [postgres]
user = "pelletier" user = "pelletier"
password = "mypassword"`) password = "mypassword"`)
@@ -42,7 +43,7 @@ user := config.Get("postgres.user").(string)
// or using an intermediate object // or using an intermediate object
postgresConfig := config.Get("postgres").(*toml.Tree) postgresConfig := config.Get("postgres").(*toml.Tree)
password = postgresConfig.Get("password").(string) password := postgresConfig.Get("password").(string)
``` ```
Or use Unmarshal: Or use Unmarshal:
@@ -57,12 +58,12 @@ type Config struct {
} }
doc := []byte(` doc := []byte(`
[postgres] [Postgres]
user = "pelletier" User = "pelletier"
password = "mypassword"`) Password = "mypassword"`)
config := Config{} config := Config{}
Unmarshal(doc, &config) toml.Unmarshal(doc, &config)
fmt.Println("user=", config.Postgres.User) fmt.Println("user=", config.Postgres.User)
``` ```
@@ -70,22 +71,23 @@ Or use a query:
```go ```go
// use a query to gather elements without walking the tree // use a query to gather elements without walking the tree
results, _ := config.Query("$..[user,password]") q, _ := query.Compile("$..[user,password]")
results := q.Execute(config)
for ii, item := range results.Values() { for ii, item := range results.Values() {
fmt.Println("Query result %d: %v", ii, item) fmt.Printf("Query result %d: %v\n", ii, item)
} }
``` ```
## Documentation ## Documentation
The documentation and additional examples are available at The documentation and additional examples are available at
[godoc.org](http://godoc.org/github.com/pelletier/go-toml). [pkg.go.dev](https://pkg.go.dev/github.com/pelletier/go-toml).
## Tools ## Tools
Go-toml provides two handy command line tools: Go-toml provides three handy command line tools:
* `tomll`: Reads TOML files and lint them. * `tomll`: Reads TOML files and lints them.
``` ```
go install github.com/pelletier/go-toml/cmd/tomll go install github.com/pelletier/go-toml/cmd/tomll
@@ -98,6 +100,30 @@ Go-toml provides two handy command line tools:
tomljson --help 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 available 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
```
Only master (`latest`) and tagged versions are published to dockerhub. You
can build your own image as usual:
```
docker build -t go-toml .
```
## Contribute ## Contribute
Feel free to report bugs and patches using GitHub's pull requests system on Feel free to report bugs and patches using GitHub's pull requests system on
@@ -106,12 +132,19 @@ much appreciated!
### Run tests ### Run tests
You have to make sure two kind of tests run: `go test ./...`
1. The Go unit tests ### Fuzzing
2. The TOML examples base
You can run both of them using `./test.sh`. The script `./fuzz.sh` is available to
run [go-fuzz](https://github.com/dvyukov/go-fuzz) on go-toml.
## Versioning
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)).
## License ## License
+188
View File
@@ -0,0 +1,188 @@
trigger:
- master
stages:
- stage: run_checks
displayName: "Check"
dependsOn: []
jobs:
- job: fmt
displayName: "fmt"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.16"
inputs:
version: "1.16"
- 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.16"
inputs:
version: "1.16"
- 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}'
env:
CODECOV_TOKEN: $(CODECOV_TOKEN)
- job: benchmark
displayName: "benchmark"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.16"
inputs:
version: "1.16"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- task: Bash@3
inputs:
filePath: './benchmark.sh'
arguments: "master $(Build.Repository.Uri)"
- job: go_unit_tests
displayName: "unit tests"
strategy:
matrix:
linux 1.16:
goVersion: '1.16'
imageName: 'ubuntu-latest'
mac 1.16:
goVersion: '1.16'
imageName: 'macOS-latest'
windows 1.16:
goVersion: '1.16'
imageName: 'windows-latest'
linux 1.15:
goVersion: '1.15'
imageName: 'ubuntu-latest'
mac 1.15:
goVersion: '1.15'
imageName: 'macOS-latest'
windows 1.15:
goVersion: '1.15'
imageName: 'windows-latest'
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_binaries
displayName: "Build binaries"
dependsOn: run_checks
jobs:
- job: build_binary
displayName: "Build binary"
strategy:
matrix:
linux_amd64:
GOOS: linux
GOARCH: amd64
darwin_amd64:
GOOS: darwin
GOARCH: amd64
windows_amd64:
GOOS: windows
GOARCH: amd64
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go"
inputs:
version: 1.16
- task: Bash@3
inputs:
targetType: inline
script: "make dist"
env:
go.goos: $(GOOS)
go.goarch: $(GOARCH)
- task: CopyFiles@2
inputs:
sourceFolder: '$(Build.SourcesDirectory)'
contents: '*.tar.xz'
TargetFolder: '$(Build.ArtifactStagingDirectory)'
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: binaries
- stage: build_binaries_manifest
displayName: "Build binaries manifest"
dependsOn: build_binaries
jobs:
- job: build_manifest
displayName: "Build binaries manifest"
steps:
- task: DownloadBuildArtifacts@0
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'binaries'
downloadPath: '$(Build.SourcesDirectory)'
- task: Bash@3
inputs:
targetType: inline
script: "cd binaries && sha256sum --binary *.tar.xz | tee $(Build.ArtifactStagingDirectory)/sha256sums.txt"
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: manifest
- stage: build_docker_image
displayName: "Build Docker image"
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'
Executable
+35
View File
@@ -0,0 +1,35 @@
#!/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}
cd benchmark
go test -bench=. -benchmem | tee -a ${ref_benchmark}
popd >/dev/null
echo ""
echo "=== local"
go test -bench=. -benchmem | tee ${local_benchmark}
cd benchmark
go test -bench=. -benchmem | tee -a ${local_benchmark}
echo ""
echo "=== diff"
benchstat -delta-test=none ${ref_benchmark} ${local_benchmark}
+164
View File
@@ -0,0 +1,164 @@
{
"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": {}
}
}
}
}
+244
View File
@@ -0,0 +1,244 @@
################################################################################
## Comment
# Speak your mind with the hash symbol. They go from the symbol to the end of
# the line.
################################################################################
## Table
# Tables (also known as hash tables or dictionaries) are collections of
# key/value pairs. They appear in square brackets on a line by themselves.
[table]
key = "value" # Yeah, you can do this.
# Nested tables are denoted by table names with dots in them. Name your tables
# whatever crap you please, just don't use #, ., [ or ].
[table.subtable]
key = "another value"
# You don't need to specify all the super-tables if you don't want to. TOML
# knows how to do it for you.
# [x] you
# [x.y] don't
# [x.y.z] need these
[x.y.z.w] # for this to work
################################################################################
## Inline Table
# Inline tables provide a more compact syntax for expressing tables. They are
# especially useful for grouped data that can otherwise quickly become verbose.
# Inline tables are enclosed in curly braces `{` and `}`. No newlines are
# allowed between the curly braces unless they are valid within a value.
[table.inline]
name = { first = "Tom", last = "Preston-Werner" }
point = { x = 1, y = 2 }
################################################################################
## String
# There are four ways to express strings: basic, multi-line basic, literal, and
# multi-line literal. All strings must contain only valid UTF-8 characters.
[string.basic]
basic = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."
[string.multiline]
# The following strings are byte-for-byte equivalent:
key1 = "One\nTwo"
key2 = """One\nTwo"""
key3 = """
One
Two"""
[string.multiline.continued]
# The following strings are byte-for-byte equivalent:
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.\
"""
[string.literal]
# What you see is what you get.
winpath = 'C:\Users\nodejs\templates'
winpath2 = '\\ServerX\admin$\system32\'
quoted = 'Tom "Dubs" Preston-Werner'
regex = '<\i\c*\s*>'
[string.literal.multiline]
regex2 = '''I [dw]on't need \d{2} apples'''
lines = '''
The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
'''
################################################################################
## Integer
# Integers are whole numbers. Positive numbers may be prefixed with a plus sign.
# Negative numbers are prefixed with a minus sign.
[integer]
key1 = +99
key2 = 42
key3 = 0
key4 = -17
[integer.underscores]
# For large numbers, you may use underscores to enhance readability. Each
# underscore must be surrounded by at least one digit.
key1 = 1_000
key2 = 5_349_221
key3 = 1_2_3_4_5 # valid but inadvisable
################################################################################
## Float
# A float consists of an integer part (which may be prefixed with a plus or
# minus sign) followed by a fractional part and/or an exponent part.
[float.fractional]
key1 = +1.0
key2 = 3.1415
key3 = -0.01
[float.exponent]
key1 = 5e+22
key2 = 1e6
key3 = -2E-2
[float.both]
key = 6.626e-34
[float.underscores]
key1 = 9_224_617.445_991_228_313
key2 = 1e1_00
################################################################################
## Boolean
# Booleans are just the tokens you're used to. Always lowercase.
[boolean]
True = true
False = false
################################################################################
## Datetime
# Datetimes are RFC 3339 dates.
[datetime]
key1 = 1979-05-27T07:32:00Z
key2 = 1979-05-27T00:32:00-07:00
key3 = 1979-05-27T00:32:00.999999-07:00
################################################################################
## Array
# Arrays are square brackets with other primitives inside. Whitespace is
# ignored. Elements are separated by commas. Data types may not be mixed.
[array]
key1 = [ 1, 2, 3 ]
key2 = [ "red", "yellow", "green" ]
key3 = [ [ 1, 2 ], [3, 4, 5] ]
#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
# the closing bracket.
key5 = [
1, 2, 3
]
key6 = [
1,
2, # this is ok
]
################################################################################
## Array of Tables
# These can be expressed by using a table name in double brackets. Each table
# with the same double bracketed name will be an element in the array. The
# tables are inserted in the order encountered.
[[products]]
name = "Hammer"
sku = 738594937
[[products]]
[[products]]
name = "Nail"
sku = 284758393
color = "gray"
# You can create nested arrays of tables as well.
[[fruit]]
name = "apple"
[fruit.physical]
color = "red"
shape = "round"
[[fruit.variety]]
name = "red delicious"
[[fruit.variety]]
name = "granny smith"
[[fruit]]
name = "banana"
[[fruit.variety]]
name = "plantain"
+121
View File
@@ -0,0 +1,121 @@
---
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: {}
+194
View File
@@ -0,0 +1,194 @@
package benchmark
import (
"bytes"
"encoding/json"
"io/ioutil"
"testing"
"time"
burntsushi "github.com/BurntSushi/toml"
"github.com/pelletier/go-toml"
"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 := toml.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.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
target := benchmarkDoc{}
err := toml.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)
}
}
}
+11
View File
@@ -0,0 +1,11 @@
module github.com/pelletier/go-toml/benchmark
go 1.12
require (
github.com/BurntSushi/toml v0.3.1
github.com/pelletier/go-toml v0.0.0
gopkg.in/yaml.v2 v2.3.0
)
replace github.com/pelletier/go-toml => ../
+6
View File
@@ -0,0 +1,6 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+82
View File
@@ -0,0 +1,82 @@
// 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
@@ -0,0 +1,92 @@
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)
}
-91
View File
@@ -1,91 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"time"
"github.com/pelletier/go-toml"
)
func main() {
bytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
log.Fatalf("Error during TOML read: %s", err)
os.Exit(2)
}
tree, err := toml.Load(string(bytes))
if err != nil {
log.Fatalf("Error during TOML load: %s", err)
os.Exit(1)
}
typedTree := translate(*tree)
if err := json.NewEncoder(os.Stdout).Encode(typedTree); err != nil {
log.Fatalf("Error encoding JSON: %s", err)
os.Exit(3)
}
os.Exit(0)
}
func translate(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] = translate(v)
}
return typed
case *toml.Tree:
return translate(*orig)
case toml.Tree:
keys := orig.Keys()
typed := make(map[string]interface{}, len(keys))
for _, k := range keys {
typed[k] = translate(orig.GetPath([]string{k}))
}
return typed
case []*toml.Tree:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v).(map[string]interface{})
}
return typed
case []map[string]interface{}:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v).(map[string]interface{})
}
return typed
case []interface{}:
typed := make([]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v)
}
return tag("array", typed)
case time.Time:
return tag("datetime", orig.Format("2006-01-02T15:04:05Z"))
case bool:
return tag("bool", fmt.Sprintf("%v", orig))
case int64:
return tag("integer", fmt.Sprintf("%d", orig))
case float64:
return tag("float", fmt.Sprintf("%v", orig))
case string:
return tag("string", orig)
}
panic(fmt.Sprintf("Unknown type: %T", tomlData))
}
func tag(typeName string, data interface{}) map[string]interface{} {
return map[string]interface{}{
"type": typeName,
"value": data,
}
}
+6 -7
View File
@@ -17,13 +17,12 @@ import (
func main() { func main() {
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintln(os.Stderr, `tomljson can be used in two ways: fmt.Fprintln(os.Stderr, "tomljson can be used in two ways:")
Writing to STDIN and reading from STDOUT: fmt.Fprintln(os.Stderr, "Writing to STDIN and reading from STDOUT:")
cat file.toml | tomljson > file.json fmt.Fprintln(os.Stderr, " cat file.toml | tomljson > file.json")
fmt.Fprintln(os.Stderr, "")
Reading from a file name: fmt.Fprintln(os.Stderr, "Reading from a file name:")
tomljson file.toml fmt.Fprintln(os.Stderr, " tomljson file.toml")
`)
} }
flag.Parse() flag.Parse()
os.Exit(processMain(flag.Args(), os.Stdin, os.Stdout, os.Stderr)) os.Exit(processMain(flag.Args(), os.Stdin, os.Stdout, os.Stderr))
+9 -1
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"io/ioutil" "io/ioutil"
"os" "os"
"runtime"
"strings" "strings"
"testing" "testing"
) )
@@ -76,7 +77,14 @@ func TestProcessMainReadFromFile(t *testing.T) {
} }
func TestProcessMainReadFromMissingFile(t *testing.T) { func TestProcessMainReadFromMissingFile(t *testing.T) {
expectedError := `open /this/file/does/not/exist: no such file or directory 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) expectProcessMainResults(t, ``, []string{"/this/file/does/not/exist"}, -1, ``, expectedError)
} }
+8 -9
View File
@@ -17,15 +17,14 @@ import (
func main() { func main() {
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintln(os.Stderr, `tomll can be used in two ways: fmt.Fprintln(os.Stderr, "tomll can be used in two ways:")
Writing to STDIN and reading from STDOUT: fmt.Fprintln(os.Stderr, "Writing to STDIN and reading from STDOUT:")
cat file.toml | tomll > file.toml fmt.Fprintln(os.Stderr, " cat file.toml | tomll > file.toml")
fmt.Fprintln(os.Stderr, "")
Reading and updating a list of files: fmt.Fprintln(os.Stderr, "Reading and updating a list of files:")
tomll a.toml b.toml c.toml fmt.Fprintln(os.Stderr, " tomll a.toml b.toml c.toml")
fmt.Fprintln(os.Stderr, "")
When given a list of files, tomll will modify all files in place without asking. fmt.Fprintln(os.Stderr, "When given a list of files, tomll will modify all files in place without asking.")
`)
} }
flag.Parse() flag.Parse()
// read from stdin and print to stdout // read from stdin and print to stdout
+219
View File
@@ -0,0 +1,219 @@
// 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))
}
+2 -2
View File
@@ -1,7 +1,7 @@
// Package toml is a TOML parser and manipulation library. // Package toml is a TOML parser and manipulation library.
// //
// This version supports the specification as described in // This version supports the specification as described in
// https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md // https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md
// //
// Marshaling // Marshaling
// //
@@ -17,7 +17,7 @@
// JSONPath-like queries // JSONPath-like queries
// //
// The package github.com/pelletier/go-toml/query implements a system // The package github.com/pelletier/go-toml/query implements a system
// similar to JSONPath to quickly retrive elements of a TOML document using a // similar to JSONPath to quickly retrieve elements of a TOML document using a
// single expression. See the package documentation for more information. // single expression. See the package documentation for more information.
// //
package toml package toml
+125 -8
View File
@@ -1,25 +1,30 @@
// code examples for godoc // code examples for godoc
package toml package toml_test
import ( import (
"fmt" "fmt"
"log"
"os"
toml "github.com/pelletier/go-toml"
) )
func Example_tree() { func Example_tree() {
config, err := LoadFile("config.toml") config, err := toml.LoadFile("config.toml")
if err != nil { if err != nil {
fmt.Println("Error ", err.Error()) fmt.Println("Error ", err.Error())
} else { } else {
// retrieve data directly // retrieve data directly
user := config.Get("postgres.user").(string) directUser := config.Get("postgres.user").(string)
password := config.Get("postgres.password").(string) directPassword := config.Get("postgres.password").(string)
fmt.Println("User is", directUser, " and password is", directPassword)
// or using an intermediate object // or using an intermediate object
configTree := config.Get("postgres").(*Tree) configTree := config.Get("postgres").(*toml.Tree)
user = configTree.Get("user").(string) user := configTree.Get("user").(string)
password = configTree.Get("password").(string) password := configTree.Get("password").(string)
fmt.Println("User is", user, " and password is", password) fmt.Println("User is", user, " and password is", password)
// show where elements are in the file // show where elements are in the file
@@ -48,6 +53,118 @@ func Example_unmarshal() {
`) `)
person := Person{} person := Person{}
Unmarshal(document, &person) toml.Unmarshal(document, &person)
fmt.Println(person.Name, "is", person.Age, "and works at", person.Employer.Name) 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
}
func ExampleEncoder_anonymous() {
type Credentials struct {
User string `toml:"user"`
Password string `toml:"password"`
}
type Protocol struct {
Name string `toml:"name"`
}
type Config struct {
Version int `toml:"version"`
Credentials
Protocol `toml:"Protocol"`
}
config := Config{
Version: 2,
Credentials: Credentials{
User: "pelletier",
Password: "mypassword",
},
Protocol: Protocol{
Name: "tcp",
},
}
fmt.Println("Default:")
fmt.Println("---------------")
def := toml.NewEncoder(os.Stdout)
if err := def.Encode(config); err != nil {
log.Fatal(err)
}
fmt.Println("---------------")
fmt.Println("With promotion:")
fmt.Println("---------------")
prom := toml.NewEncoder(os.Stdout).PromoteAnonymous(true)
if err := prom.Encode(config); err != nil {
log.Fatal(err)
}
// Output:
// Default:
// ---------------
// password = "mypassword"
// user = "pelletier"
// version = 2
//
// [Protocol]
// name = "tcp"
// ---------------
// With promotion:
// ---------------
// version = 2
//
// [Credentials]
// password = "mypassword"
// user = "pelletier"
//
// [Protocol]
// name = "tcp"
} }
+1
View File
@@ -27,3 +27,4 @@ enabled = true
[clients] [clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
score = 4e-08 # to make sure leading zeroes in exponent parts of floats are supported
+1
View File
@@ -27,3 +27,4 @@ enabled = true
[clients] [clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
score = 4e-08 # to make sure leading zeroes in exponent parts of floats are supported
+31
View File
@@ -0,0 +1,31 @@
// +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
}
Executable
+15
View File
@@ -0,0 +1,15 @@
#! /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
+3
View File
@@ -0,0 +1,3 @@
module github.com/pelletier/go-toml
go 1.12
+84 -66
View File
@@ -3,92 +3,110 @@
package toml package toml
import ( import (
"bytes"
"errors" "errors"
"fmt" "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) { func parseKey(key string) ([]string, error) {
groups := []string{} runes := []rune(key)
var buffer bytes.Buffer var groups []string
inQuotes := false
wasInQuotes := false
escapeNext := false
ignoreSpace := true
expectDot := false
for _, char := range key { if len(key) == 0 {
if ignoreSpace { return nil, errors.New("empty key")
if char == ' ' { }
continue
} idx := 0
ignoreSpace = false for idx < len(runes) {
for ; idx < len(runes) && isSpace(runes[idx]); idx++ {
// skip leading whitespace
} }
if escapeNext { if idx >= len(runes) {
buffer.WriteRune(char) break
escapeNext = false
continue
} }
switch char { r := runes[idx]
case '\\': if isValidBareChar(r) {
escapeNext = true // parse bare key
continue startIdx := idx
case '"': endIdx := -1
if inQuotes { idx++
groups = append(groups, buffer.String()) for idx < len(runes) {
buffer.Reset() r = runes[idx]
wasInQuotes = true if isValidBareChar(r) {
} idx++
inQuotes = !inQuotes } else if r == '.' {
expectDot = false endIdx = idx
case '.': break
if inQuotes { } else if isSpace(r) {
buffer.WriteRune(char) endIdx = idx
} else { for ; idx < len(runes) && isSpace(runes[idx]); idx++ {
if !wasInQuotes { // skip trailing whitespace
if buffer.Len() == 0 {
return nil, errors.New("empty table key")
} }
groups = append(groups, buffer.String()) if idx < len(runes) && runes[idx] != '.' {
buffer.Reset() return nil, fmt.Errorf("invalid key character after whitespace: %c", runes[idx])
}
break
} else {
return nil, fmt.Errorf("invalid bare key character: %c", r)
} }
ignoreSpace = true
expectDot = false
wasInQuotes = false
} }
case ' ': if endIdx == -1 {
if inQuotes { endIdx = idx
buffer.WriteRune(char)
} else {
expectDot = true
} }
default: groups = append(groups, string(runes[startIdx:endIdx]))
if !inQuotes && !isValidBareChar(char) { } else if r == '\'' {
return nil, fmt.Errorf("invalid bare character: %c", char) // 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++
} }
if !inQuotes && expectDot { } else if r == '"' {
return nil, errors.New("what?") // 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++
} }
buffer.WriteRune(char) } else if r == '.' {
expectDot = false 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 inQuotes {
return nil, errors.New("mismatched quotes")
}
if escapeNext {
return nil, errors.New("unfinished escape sequence")
}
if buffer.Len() > 0 {
groups = append(groups, buffer.String())
}
if len(groups) == 0 { if len(groups) == 0 {
return nil, errors.New("empty key") return nil, fmt.Errorf("empty key")
} }
return groups, nil return groups, nil
} }
func isValidBareChar(r rune) bool { func isValidBareChar(r rune) bool {
return isAlphanumeric(r) || r == '-' || unicode.IsNumber(r) return isAlphanumeric(r) || r == '-' || isDigit(r)
} }
+27 -4
View File
@@ -22,7 +22,10 @@ func testResult(t *testing.T, key string, expected []string) {
} }
func testError(t *testing.T, key string, expectedError string) { func testError(t *testing.T, key string, expectedError string) {
_, err := parseKey(key) res, err := parseKey(key)
if err == nil {
t.Fatalf("Expected error, but successfully parsed key %s", res)
}
if fmt.Sprintf("%s", err) != expectedError { if fmt.Sprintf("%s", err) != expectedError {
t.Fatalf("Expected error \"%s\", but got \"%s\".", expectedError, err) t.Fatalf("Expected error \"%s\", but got \"%s\".", expectedError, err)
} }
@@ -41,16 +44,36 @@ func TestDottedKeyBasic(t *testing.T) {
} }
func TestBaseKeyPound(t *testing.T) { func TestBaseKeyPound(t *testing.T) {
testError(t, "hello#world", "invalid bare character: #") 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) { func TestQuotedKeys(t *testing.T) {
testResult(t, `hello."foo".bar`, []string{"hello", "foo", "bar"}) testResult(t, `hello."foo".bar`, []string{"hello", "foo", "bar"})
testResult(t, `"hello!"`, []string{"hello!"}) 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) { func TestEmptyKey(t *testing.T) {
testError(t, "", "empty key") testError(t, ``, "empty key")
testError(t, " ", "empty key") testError(t, ` `, "empty key")
testResult(t, `""`, []string{""}) testResult(t, `""`, []string{""})
} }
+481 -108
View File
@@ -9,44 +9,38 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/pelletier/go-buffruneio"
) )
var dateRegexp *regexp.Regexp
// Define state functions // Define state functions
type tomlLexStateFn func() tomlLexStateFn type tomlLexStateFn func() tomlLexStateFn
// Define lexer // Define lexer
type tomlLexer struct { type tomlLexer struct {
input *buffruneio.Reader // Textual source inputIdx int
buffer bytes.Buffer // Runes composing the current token input []rune // Textual source
tokens chan token currentTokenStart int
depth int currentTokenStop int
line int tokens []token
col int brackets []rune
endbufferLine int line int
endbufferCol int col int
endbufferLine int
endbufferCol int
} }
// Basic read operations on input // Basic read operations on input
func (l *tomlLexer) read() rune { func (l *tomlLexer) read() rune {
r, _, err := l.input.ReadRune() r := l.peek()
if err != nil {
panic(err)
}
if r == '\n' { if r == '\n' {
l.endbufferLine++ l.endbufferLine++
l.endbufferCol = 1 l.endbufferCol = 1
} else { } else {
l.endbufferCol++ l.endbufferCol++
} }
l.inputIdx++
return r return r
} }
@@ -54,13 +48,13 @@ func (l *tomlLexer) next() rune {
r := l.read() r := l.read()
if r != eof { if r != eof {
l.buffer.WriteRune(r) l.currentTokenStop++
} }
return r return r
} }
func (l *tomlLexer) ignore() { func (l *tomlLexer) ignore() {
l.buffer.Reset() l.currentTokenStart = l.currentTokenStop
l.line = l.endbufferLine l.line = l.endbufferLine
l.col = l.endbufferCol l.col = l.endbufferCol
} }
@@ -77,49 +71,46 @@ func (l *tomlLexer) fastForward(n int) {
} }
func (l *tomlLexer) emitWithValue(t tokenType, value string) { func (l *tomlLexer) emitWithValue(t tokenType, value string) {
l.tokens <- token{ l.tokens = append(l.tokens, token{
Position: Position{l.line, l.col}, Position: Position{l.line, l.col},
typ: t, typ: t,
val: value, val: value,
} })
l.ignore() l.ignore()
} }
func (l *tomlLexer) emit(t tokenType) { func (l *tomlLexer) emit(t tokenType) {
l.emitWithValue(t, l.buffer.String()) l.emitWithValue(t, string(l.input[l.currentTokenStart:l.currentTokenStop]))
} }
func (l *tomlLexer) peek() rune { func (l *tomlLexer) peek() rune {
r, _, err := l.input.ReadRune() if l.inputIdx >= len(l.input) {
if err != nil { return eof
panic(err)
} }
l.input.UnreadRune() return l.input[l.inputIdx]
return r }
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 { func (l *tomlLexer) follow(next string) bool {
for _, expectedRune := range next { return next == l.peekString(len(next))
r, _, err := l.input.ReadRune()
defer l.input.UnreadRune()
if err != nil {
panic(err)
}
if expectedRune != r {
return false
}
}
return true
} }
// Error management // Error management
func (l *tomlLexer) errorf(format string, args ...interface{}) tomlLexStateFn { func (l *tomlLexer) errorf(format string, args ...interface{}) tomlLexStateFn {
l.tokens <- token{ l.tokens = append(l.tokens, token{
Position: Position{l.line, l.col}, Position: Position{l.line, l.col},
typ: tokenError, typ: tokenError,
val: fmt.Sprintf(format, args...), val: fmt.Sprintf(format, args...),
} })
return nil return nil
} }
@@ -129,6 +120,8 @@ func (l *tomlLexer) lexVoid() tomlLexStateFn {
for { for {
next := l.peek() next := l.peek()
switch next { switch next {
case '}': // after '{'
return l.lexRightCurlyBrace
case '[': case '[':
return l.lexTableKey return l.lexTableKey
case '#': case '#':
@@ -146,10 +139,6 @@ func (l *tomlLexer) lexVoid() tomlLexStateFn {
l.skip() l.skip()
} }
if l.depth > 0 {
return l.lexRvalue
}
if isKeyStartChar(next) { if isKeyStartChar(next) {
return l.lexKey return l.lexKey
} }
@@ -173,10 +162,8 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
case '=': case '=':
return l.lexEqual return l.lexEqual
case '[': case '[':
l.depth++
return l.lexLeftBracket return l.lexLeftBracket
case ']': case ']':
l.depth--
return l.lexRightBracket return l.lexRightBracket
case '{': case '{':
return l.lexLeftCurlyBrace return l.lexLeftCurlyBrace
@@ -194,12 +181,10 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
fallthrough fallthrough
case '\n': case '\n':
l.skip() l.skip()
if l.depth == 0 { if len(l.brackets) > 0 && l.brackets[len(l.brackets)-1] == '[' {
return l.lexVoid return l.lexRvalue
} }
return l.lexRvalue return l.lexVoid
case '_':
return l.errorf("cannot start number with underscore")
} }
if l.follow("true") { if l.follow("true") {
@@ -210,6 +195,14 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
return l.lexFalse return l.lexFalse
} }
if l.follow("inf") {
return l.lexInf
}
if l.follow("nan") {
return l.lexNan
}
if isSpace(next) { if isSpace(next) {
l.skip() l.skip()
continue continue
@@ -220,19 +213,12 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
break break
} }
possibleDate := string(l.input.PeekRunes(35)) if next == '+' || next == '-' {
dateMatch := dateRegexp.FindString(possibleDate)
if dateMatch != "" {
l.fastForward(len(dateMatch))
return l.lexDate
}
if next == '+' || next == '-' || isDigit(next) {
return l.lexNumber return l.lexNumber
} }
if isAlphanumeric(next) { if isDigit(next) {
return l.lexKey return l.lexDateTimeOrNumber
} }
return l.errorf("no value can start with %c", next) return l.errorf("no value can start with %c", next)
@@ -242,23 +228,290 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
return nil return nil
} }
func (l *tomlLexer) lexDateTimeOrNumber() tomlLexStateFn {
// Could be either a date/time, or a digit.
// The options for date/times are:
// YYYY-... => date or date-time
// HH:... => time
// Anything else should be a number.
lookAhead := l.peekString(5)
if len(lookAhead) < 3 {
return l.lexNumber()
}
for idx, r := range lookAhead {
if !isDigit(r) {
if idx == 2 && r == ':' {
return l.lexDateTimeOrTime()
}
if idx == 4 && r == '-' {
return l.lexDateTimeOrTime()
}
return l.lexNumber()
}
}
return l.lexNumber()
}
func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn { func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn {
l.next() l.next()
l.emit(tokenLeftCurlyBrace) l.emit(tokenLeftCurlyBrace)
return l.lexRvalue l.brackets = append(l.brackets, '{')
return l.lexVoid
} }
func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn { func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn {
l.next() l.next()
l.emit(tokenRightCurlyBrace) l.emit(tokenRightCurlyBrace)
if len(l.brackets) == 0 || l.brackets[len(l.brackets)-1] != '{' {
return l.errorf("cannot have '}' here")
}
l.brackets = l.brackets[:len(l.brackets)-1]
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexDate() tomlLexStateFn { func (l *tomlLexer) lexDateTimeOrTime() tomlLexStateFn {
l.emit(tokenDate) // Example matches:
// 1979-05-27T07:32:00Z
// 1979-05-27T00:32:00-07:00
// 1979-05-27T00:32:00.999999-07:00
// 1979-05-27 07:32:00Z
// 1979-05-27 00:32:00-07:00
// 1979-05-27 00:32:00.999999-07:00
// 1979-05-27T07:32:00
// 1979-05-27T00:32:00.999999
// 1979-05-27 07:32:00
// 1979-05-27 00:32:00.999999
// 1979-05-27
// 07:32:00
// 00:32:00.999999
// we already know those two are digits
l.next()
l.next()
// Got 2 digits. At that point it could be either a time or a date(-time).
r := l.next()
if r == ':' {
return l.lexTime()
}
return l.lexDateTime()
}
func (l *tomlLexer) lexDateTime() tomlLexStateFn {
// This state accepts an offset date-time, a local date-time, or a local date.
//
// v--- cursor
// 1979-05-27T07:32:00Z
// 1979-05-27T00:32:00-07:00
// 1979-05-27T00:32:00.999999-07:00
// 1979-05-27 07:32:00Z
// 1979-05-27 00:32:00-07:00
// 1979-05-27 00:32:00.999999-07:00
// 1979-05-27T07:32:00
// 1979-05-27T00:32:00.999999
// 1979-05-27 07:32:00
// 1979-05-27 00:32:00.999999
// 1979-05-27
// date
// already checked by lexRvalue
l.next() // digit
l.next() // -
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid month digit in date: %c", r)
}
}
r := l.next()
if r != '-' {
return l.errorf("expected - to separate month of a date, not %c", r)
}
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid day digit in date: %c", r)
}
}
l.emit(tokenLocalDate)
r = l.peek()
if r == eof {
return l.lexRvalue
}
if r != ' ' && r != 'T' {
return l.errorf("incorrect date/time separation character: %c", r)
}
if r == ' ' {
lookAhead := l.peekString(3)[1:]
if len(lookAhead) < 2 {
return l.lexRvalue
}
for _, r := range lookAhead {
if !isDigit(r) {
return l.lexRvalue
}
}
}
l.skip() // skip the T or ' '
// time
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid hour digit in time: %c", r)
}
}
r = l.next()
if r != ':' {
return l.errorf("time hour/minute separator should be :, not %c", r)
}
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid minute digit in time: %c", r)
}
}
r = l.next()
if r != ':' {
return l.errorf("time minute/second separator should be :, not %c", r)
}
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid second digit in time: %c", r)
}
}
r = l.peek()
if r == '.' {
l.next()
r := l.next()
if !isDigit(r) {
return l.errorf("expected at least one digit in time's fraction, not %c", r)
}
for {
r := l.peek()
if !isDigit(r) {
break
}
l.next()
}
}
l.emit(tokenLocalTime)
return l.lexTimeOffset
}
func (l *tomlLexer) lexTimeOffset() tomlLexStateFn {
// potential offset
// Z
// -07:00
// +07:00
// nothing
r := l.peek()
if r == 'Z' {
l.next()
l.emit(tokenTimeOffset)
} else if r == '+' || r == '-' {
l.next()
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid hour digit in time offset: %c", r)
}
}
r = l.next()
if r != ':' {
return l.errorf("time offset hour/minute separator should be :, not %c", r)
}
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid minute digit in time offset: %c", r)
}
}
l.emit(tokenTimeOffset)
}
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexTime() tomlLexStateFn {
// v--- cursor
// 07:32:00
// 00:32:00.999999
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid minute digit in time: %c", r)
}
}
r := l.next()
if r != ':' {
return l.errorf("time minute/second separator should be :, not %c", r)
}
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid second digit in time: %c", r)
}
}
r = l.peek()
if r == '.' {
l.next()
r := l.next()
if !isDigit(r) {
return l.errorf("expected at least one digit in time's fraction, not %c", r)
}
for {
r := l.peek()
if !isDigit(r) {
break
}
l.next()
}
}
l.emit(tokenLocalTime)
return l.lexRvalue
}
func (l *tomlLexer) lexTrue() tomlLexStateFn { func (l *tomlLexer) lexTrue() tomlLexStateFn {
l.fastForward(4) l.fastForward(4)
l.emit(tokenTrue) l.emit(tokenTrue)
@@ -271,6 +524,18 @@ func (l *tomlLexer) lexFalse() tomlLexStateFn {
return l.lexRvalue 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 { func (l *tomlLexer) lexEqual() tomlLexStateFn {
l.next() l.next()
l.emit(tokenEqual) l.emit(tokenEqual)
@@ -280,11 +545,16 @@ func (l *tomlLexer) lexEqual() tomlLexStateFn {
func (l *tomlLexer) lexComma() tomlLexStateFn { func (l *tomlLexer) lexComma() tomlLexStateFn {
l.next() l.next()
l.emit(tokenComma) l.emit(tokenComma)
if len(l.brackets) > 0 && l.brackets[len(l.brackets)-1] == '{' {
return l.lexVoid
}
return l.lexRvalue 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 { func (l *tomlLexer) lexKey() tomlLexStateFn {
growingString := "" var sb strings.Builder
for r := l.peek(); isKeyChar(r) || r == '\n' || r == '\r'; r = l.peek() { for r := l.peek(); isKeyChar(r) || r == '\n' || r == '\r'; r = l.peek() {
if r == '"' { if r == '"' {
@@ -293,20 +563,56 @@ func (l *tomlLexer) lexKey() tomlLexStateFn {
if err != nil { if err != nil {
return l.errorf(err.Error()) return l.errorf(err.Error())
} }
growingString += `"` + str + `"` sb.WriteString("\"")
sb.WriteString(str)
sb.WriteString("\"")
l.next()
continue
} else if r == '\'' {
l.next()
str, err := l.lexLiteralStringAsString(`'`, false)
if err != nil {
return l.errorf(err.Error())
}
sb.WriteString("'")
sb.WriteString(str)
sb.WriteString("'")
l.next() l.next()
continue continue
} else if r == '\n' { } else if r == '\n' {
return l.errorf("keys cannot contain new lines") return l.errorf("keys cannot contain new lines")
} else if isSpace(r) { } else if isSpace(r) {
break var str strings.Builder
str.WriteString(" ")
// skip trailing whitespace
l.next()
for r = l.peek(); isSpace(r); r = l.peek() {
str.WriteRune(r)
l.next()
}
// break loop if not a dot
if r != '.' {
break
}
str.WriteString(".")
// skip trailing whitespace after dot
l.next()
for r = l.peek(); isSpace(r); r = l.peek() {
str.WriteRune(r)
l.next()
}
sb.WriteString(str.String())
continue
} else if r == '.' {
// skip
} else if !isValidBareChar(r) { } else if !isValidBareChar(r) {
return l.errorf("keys cannot contain %c character", r) return l.errorf("keys cannot contain %c character", r)
} }
growingString += string(r) sb.WriteRune(r)
l.next() l.next()
} }
l.emitWithValue(tokenKey, growingString) l.emitWithValue(tokenKey, sb.String())
return l.lexVoid return l.lexVoid
} }
@@ -326,11 +632,12 @@ func (l *tomlLexer) lexComment(previousState tomlLexStateFn) tomlLexStateFn {
func (l *tomlLexer) lexLeftBracket() tomlLexStateFn { func (l *tomlLexer) lexLeftBracket() tomlLexStateFn {
l.next() l.next()
l.emit(tokenLeftBracket) l.emit(tokenLeftBracket)
l.brackets = append(l.brackets, '[')
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNewLine bool) (string, error) { func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNewLine bool) (string, error) {
growingString := "" var sb strings.Builder
if discardLeadingNewLine { if discardLeadingNewLine {
if l.follow("\r\n") { if l.follow("\r\n") {
@@ -344,14 +651,14 @@ func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNe
// find end of string // find end of string
for { for {
if l.follow(terminator) { if l.follow(terminator) {
return growingString, nil return sb.String(), nil
} }
next := l.peek() next := l.peek()
if next == eof { if next == eof {
break break
} }
growingString += string(l.next()) sb.WriteRune(l.next())
} }
return "", errors.New("unclosed string") return "", errors.New("unclosed string")
@@ -385,7 +692,7 @@ func (l *tomlLexer) lexLiteralString() tomlLexStateFn {
// Terminator is the substring indicating the end of the token. // Terminator is the substring indicating the end of the token.
// The resulting string does not include the terminator. // The resulting string does not include the terminator.
func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine, acceptNewLines bool) (string, error) { func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine, acceptNewLines bool) (string, error) {
growingString := "" var sb strings.Builder
if discardLeadingNewLine { if discardLeadingNewLine {
if l.follow("\r\n") { if l.follow("\r\n") {
@@ -398,7 +705,7 @@ func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine,
for { for {
if l.follow(terminator) { if l.follow(terminator) {
return growingString, nil return sb.String(), nil
} }
if l.follow("\\") { if l.follow("\\") {
@@ -416,72 +723,72 @@ func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine,
l.next() l.next()
} }
case '"': case '"':
growingString += "\"" sb.WriteString("\"")
l.next() l.next()
case 'n': case 'n':
growingString += "\n" sb.WriteString("\n")
l.next() l.next()
case 'b': case 'b':
growingString += "\b" sb.WriteString("\b")
l.next() l.next()
case 'f': case 'f':
growingString += "\f" sb.WriteString("\f")
l.next() l.next()
case '/': case '/':
growingString += "/" sb.WriteString("/")
l.next() l.next()
case 't': case 't':
growingString += "\t" sb.WriteString("\t")
l.next() l.next()
case 'r': case 'r':
growingString += "\r" sb.WriteString("\r")
l.next() l.next()
case '\\': case '\\':
growingString += "\\" sb.WriteString("\\")
l.next() l.next()
case 'u': case 'u':
l.next() l.next()
code := "" var code strings.Builder
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
c := l.peek() c := l.peek()
if !isHexDigit(c) { if !isHexDigit(c) {
return "", errors.New("unfinished unicode escape") return "", errors.New("unfinished unicode escape")
} }
l.next() l.next()
code = code + string(c) code.WriteRune(c)
} }
intcode, err := strconv.ParseInt(code, 16, 32) intcode, err := strconv.ParseInt(code.String(), 16, 32)
if err != nil { if err != nil {
return "", errors.New("invalid unicode escape: \\u" + code) return "", errors.New("invalid unicode escape: \\u" + code.String())
} }
growingString += string(rune(intcode)) sb.WriteRune(rune(intcode))
case 'U': case 'U':
l.next() l.next()
code := "" var code strings.Builder
for i := 0; i < 8; i++ { for i := 0; i < 8; i++ {
c := l.peek() c := l.peek()
if !isHexDigit(c) { if !isHexDigit(c) {
return "", errors.New("unfinished unicode escape") return "", errors.New("unfinished unicode escape")
} }
l.next() l.next()
code = code + string(c) code.WriteRune(c)
} }
intcode, err := strconv.ParseInt(code, 16, 64) intcode, err := strconv.ParseInt(code.String(), 16, 64)
if err != nil { if err != nil {
return "", errors.New("invalid unicode escape: \\U" + code) return "", errors.New("invalid unicode escape: \\U" + code.String())
} }
growingString += string(rune(intcode)) sb.WriteRune(rune(intcode))
default: default:
return "", errors.New("invalid escape sequence: \\" + string(l.peek())) return "", errors.New("invalid escape sequence: \\" + string(l.peek()))
} }
} else { } else {
r := l.peek() r := l.peek()
if 0x00 <= r && r <= 0x1F && !(acceptNewLines && (r == '\n' || r == '\r')) { if 0x00 <= r && r <= 0x1F && r != '\t' && !(acceptNewLines && (r == '\n' || r == '\r')) {
return "", fmt.Errorf("unescaped control character %U", r) return "", fmt.Errorf("unescaped control character %U", r)
} }
l.next() l.next()
growingString += string(r) sb.WriteRune(r)
} }
if l.peek() == eof { if l.peek() == eof {
@@ -508,7 +815,6 @@ func (l *tomlLexer) lexString() tomlLexStateFn {
} }
str, err := l.lexStringAsString(terminator, discardLeadingNewLine, acceptNewLines) str, err := l.lexStringAsString(terminator, discardLeadingNewLine, acceptNewLines)
if err != nil { if err != nil {
return l.errorf(err.Error()) return l.errorf(err.Error())
} }
@@ -533,11 +839,12 @@ func (l *tomlLexer) lexTableKey() tomlLexStateFn {
return l.lexInsideTableKey return l.lexInsideTableKey
} }
// Parse the key till "]]", but only bare keys are supported
func (l *tomlLexer) lexInsideTableArrayKey() tomlLexStateFn { func (l *tomlLexer) lexInsideTableArrayKey() tomlLexStateFn {
for r := l.peek(); r != eof; r = l.peek() { for r := l.peek(); r != eof; r = l.peek() {
switch r { switch r {
case ']': case ']':
if l.buffer.Len() > 0 { if l.currentTokenStop > l.currentTokenStart {
l.emit(tokenKeyGroupArray) l.emit(tokenKeyGroupArray)
} }
l.next() l.next()
@@ -556,11 +863,12 @@ func (l *tomlLexer) lexInsideTableArrayKey() tomlLexStateFn {
return l.errorf("unclosed table array key") return l.errorf("unclosed table array key")
} }
// Parse the key till "]" but only bare keys are supported
func (l *tomlLexer) lexInsideTableKey() tomlLexStateFn { func (l *tomlLexer) lexInsideTableKey() tomlLexStateFn {
for r := l.peek(); r != eof; r = l.peek() { for r := l.peek(); r != eof; r = l.peek() {
switch r { switch r {
case ']': case ']':
if l.buffer.Len() > 0 { if l.currentTokenStop > l.currentTokenStart {
l.emit(tokenKeyGroup) l.emit(tokenKeyGroup)
} }
l.next() l.next()
@@ -578,14 +886,84 @@ func (l *tomlLexer) lexInsideTableKey() tomlLexStateFn {
func (l *tomlLexer) lexRightBracket() tomlLexStateFn { func (l *tomlLexer) lexRightBracket() tomlLexStateFn {
l.next() l.next()
l.emit(tokenRightBracket) l.emit(tokenRightBracket)
if len(l.brackets) == 0 || l.brackets[len(l.brackets)-1] != '[' {
return l.errorf("cannot have ']' here")
}
l.brackets = l.brackets[:len(l.brackets)-1]
return l.lexRvalue 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 { func (l *tomlLexer) lexNumber() tomlLexStateFn {
r := l.peek() 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 == '-' { if r == '+' || r == '-' {
l.next() l.next()
if l.follow("inf") {
return l.lexInf
}
if l.follow("nan") {
return l.lexNan
}
} }
pointSeen := false pointSeen := false
expSeen := false expSeen := false
digitSeen := false digitSeen := false
@@ -635,24 +1013,19 @@ func (l *tomlLexer) run() {
for state := l.lexVoid; state != nil; { for state := l.lexVoid; state != nil; {
state = state() state = state()
} }
close(l.tokens)
}
func init() {
dateRegexp = regexp.MustCompile(`^\d{1,4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})`)
} }
// Entry point // Entry point
func lexToml(input io.Reader) chan token { func lexToml(inputBytes []byte) []token {
bufferedInput := buffruneio.NewReader(input) runes := bytes.Runes(inputBytes)
l := &tomlLexer{ l := &tomlLexer{
input: bufferedInput, input: runes,
tokens: make(chan token), tokens: make([]token, 0, 256),
line: 1, line: 1,
col: 1, col: 1,
endbufferLine: 1, endbufferLine: 1,
endbufferCol: 1, endbufferCol: 1,
} }
go l.run() l.run()
return l.tokens return l.tokens
} }
+532 -64
View File
@@ -1,39 +1,61 @@
package toml package toml
import ( import (
"os" "bytes"
"strings" "fmt"
"reflect"
"testing" "testing"
"text/tabwriter"
) )
func testFlow(t *testing.T, input string, expectedFlow []token) { func testFlow(t *testing.T, input string, expectedFlow []token) {
ch := lexToml(strings.NewReader(input)) tokens := lexToml([]byte(input))
for _, expected := range expectedFlow {
token := <-ch if !reflect.DeepEqual(tokens, expectedFlow) {
if token != expected { diffFlowsColumnsFatal(t, expectedFlow, tokens)
t.Log("While testing: ", 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)) func diffFlowsColumnsFatal(t *testing.T, expectedFlow []token, actualFlow []token) {
t.Log("\ttype:", token.typ.String(), "<->", expected.typ.String()) max := len(expectedFlow)
t.Log("\tline:", token.Line, "<->", expected.Line) if len(actualFlow) > max {
t.Log("\tcolumn:", token.Col, "<->", expected.Col) max = len(actualFlow)
t.Log("compared", token, "to", expected)
t.FailNow()
}
} }
tok, ok := <-ch b := &bytes.Buffer{}
if ok { w := tabwriter.NewWriter(b, 0, 0, 1, ' ', tabwriter.Debug)
t.Log("channel is not closed!")
t.Log(len(ch)+1, "tokens remaining:")
t.Log("token ->", tok) fmt.Fprintln(w, "expected\tT\tP\tactual\tT\tP\tdiff")
for token := range ch {
t.Log("token ->", token) for i := 0; i < max; i++ {
expected := ""
expectedType := ""
expectedPos := ""
if i < len(expectedFlow) {
expected = fmt.Sprintf("%s", expectedFlow[i])
expectedType = fmt.Sprintf("%s", expectedFlow[i].typ)
expectedPos = expectedFlow[i].Position.String()
} }
t.FailNow() actual := ""
actualType := ""
actualPos := ""
if i < len(actualFlow) {
actual = fmt.Sprintf("%s", actualFlow[i])
actualType = fmt.Sprintf("%s", actualFlow[i].typ)
actualPos = actualFlow[i].Position.String()
}
different := ""
if i >= len(expectedFlow) {
different = "+"
} else if i >= len(actualFlow) {
different = "-"
} else if !reflect.DeepEqual(expectedFlow[i], actualFlow[i]) {
different = "x"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", expected, expectedType, expectedPos, actual, actualType, actualPos, different)
} }
w.Flush()
t.Errorf("Different flows:\n%s", b.String())
} }
func TestValidKeyGroup(t *testing.T) { func TestValidKeyGroup(t *testing.T) {
@@ -46,11 +68,20 @@ func TestValidKeyGroup(t *testing.T) {
} }
func TestNestedQuotedUnicodeKeyGroup(t *testing.T) { func TestNestedQuotedUnicodeKeyGroup(t *testing.T) {
testFlow(t, `[ j . "ʞ" . l ]`, []token{ testFlow(t, `[ j . "ʞ" . l . 'ɯ' ]`, []token{
{Position{1, 1}, tokenLeftBracket, "["}, {Position{1, 1}, tokenLeftBracket, "["},
{Position{1, 2}, tokenKeyGroup, ` j . "ʞ" . l `}, {Position{1, 2}, tokenKeyGroup, ` j . "ʞ" . l . 'ɯ' `},
{Position{1, 15}, tokenRightBracket, "]"}, {Position{1, 21}, tokenRightBracket, "]"},
{Position{1, 16}, tokenEOF, ""}, {Position{1, 22}, tokenEOF, ""},
})
}
func TestNestedQuotedUnicodeKeyAssign(t *testing.T) {
testFlow(t, ` j . "ʞ" . l . 'ɯ' = 3`, []token{
{Position{1, 2}, tokenKey, `j . "ʞ" . l . 'ɯ'`},
{Position{1, 20}, tokenEqual, "="},
{Position{1, 22}, tokenInteger, "3"},
{Position{1, 23}, tokenEOF, ""},
}) })
} }
@@ -129,9 +160,9 @@ func TestBasicKeyWithUppercaseMix(t *testing.T) {
} }
func TestBasicKeyWithInternationalCharacters(t *testing.T) { func TestBasicKeyWithInternationalCharacters(t *testing.T) {
testFlow(t, "héllÖ", []token{ testFlow(t, "'héllÖ'", []token{
{Position{1, 1}, tokenKey, "héllÖ"}, {Position{1, 1}, tokenKey, "'héllÖ'"},
{Position{1, 6}, tokenEOF, ""}, {Position{1, 8}, tokenEOF, ""},
}) })
} }
@@ -313,36 +344,280 @@ func TestKeyEqualArrayBoolsWithComments(t *testing.T) {
}) })
} }
func TestDateRegexp(t *testing.T) {
if dateRegexp.FindString("1979-05-27T07:32:00Z") == "" {
t.Error("basic lexing")
}
if dateRegexp.FindString("1979-05-27T00:32:00-07:00") == "" {
t.Error("offset lexing")
}
if dateRegexp.FindString("1979-05-27T00:32:00.999999-07:00") == "" {
t.Error("nano precision lexing")
}
}
func TestKeyEqualDate(t *testing.T) { func TestKeyEqualDate(t *testing.T) {
testFlow(t, "foo = 1979-05-27T07:32:00Z", []token{ t.Run("local date time", func(t *testing.T) {
{Position{1, 1}, tokenKey, "foo"}, testFlow(t, "foo = 1979-05-27T07:32:00", []token{
{Position{1, 5}, tokenEqual, "="}, {Position{1, 1}, tokenKey, "foo"},
{Position{1, 7}, tokenDate, "1979-05-27T07:32:00Z"}, {Position{1, 5}, tokenEqual, "="},
{Position{1, 27}, tokenEOF, ""}, {Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "07:32:00"},
{Position{1, 26}, tokenEOF, ""},
})
}) })
testFlow(t, "foo = 1979-05-27T00:32:00-07:00", []token{
{Position{1, 1}, tokenKey, "foo"}, t.Run("local date time space", func(t *testing.T) {
{Position{1, 5}, tokenEqual, "="}, testFlow(t, "foo = 1979-05-27 07:32:00", []token{
{Position{1, 7}, tokenDate, "1979-05-27T00:32:00-07:00"}, {Position{1, 1}, tokenKey, "foo"},
{Position{1, 32}, tokenEOF, ""}, {Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "07:32:00"},
{Position{1, 26}, tokenEOF, ""},
})
}) })
testFlow(t, "foo = 1979-05-27T00:32:00.999999-07:00", []token{
{Position{1, 1}, tokenKey, "foo"}, t.Run("local date time fraction", func(t *testing.T) {
{Position{1, 5}, tokenEqual, "="}, testFlow(t, "foo = 1979-05-27T00:32:00.999999", []token{
{Position{1, 7}, tokenDate, "1979-05-27T00:32:00.999999-07:00"}, {Position{1, 1}, tokenKey, "foo"},
{Position{1, 39}, tokenEOF, ""}, {Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00.999999"},
{Position{1, 33}, tokenEOF, ""},
})
})
t.Run("local date time fraction space", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:32:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00.999999"},
{Position{1, 33}, tokenEOF, ""},
})
})
t.Run("offset date-time utc", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T07:32:00Z", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "07:32:00"},
{Position{1, 26}, tokenTimeOffset, "Z"},
{Position{1, 27}, tokenEOF, ""},
})
})
t.Run("offset date-time -07:00", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T00:32:00-07:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00"},
{Position{1, 26}, tokenTimeOffset, "-07:00"},
{Position{1, 32}, tokenEOF, ""},
})
})
t.Run("offset date-time fractions -07:00", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T00:32:00.999999-07:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00.999999"},
{Position{1, 33}, tokenTimeOffset, "-07:00"},
{Position{1, 39}, tokenEOF, ""},
})
})
t.Run("offset date-time space separated utc", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 07:32:00Z", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "07:32:00"},
{Position{1, 26}, tokenTimeOffset, "Z"},
{Position{1, 27}, tokenEOF, ""},
})
})
t.Run("offset date-time space separated offset", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:32:00-07:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00"},
{Position{1, 26}, tokenTimeOffset, "-07:00"},
{Position{1, 32}, tokenEOF, ""},
})
})
t.Run("offset date-time space separated fraction offset", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:32:00.999999-07:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00.999999"},
{Position{1, 33}, tokenTimeOffset, "-07:00"},
{Position{1, 39}, tokenEOF, ""},
})
})
t.Run("local date", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 17}, tokenEOF, ""},
})
})
t.Run("local time", func(t *testing.T) {
testFlow(t, "foo = 07:32:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalTime, "07:32:00"},
{Position{1, 15}, tokenEOF, ""},
})
})
t.Run("local time fraction", func(t *testing.T) {
testFlow(t, "foo = 00:32:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalTime, "00:32:00.999999"},
{Position{1, 22}, tokenEOF, ""},
})
})
t.Run("local time invalid minute digit", func(t *testing.T) {
testFlow(t, "foo = 00:3x:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenError, "invalid minute digit in time: x"},
})
})
t.Run("local time invalid minute/second digit", func(t *testing.T) {
testFlow(t, "foo = 00:30x00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenError, "time minute/second separator should be :, not x"},
})
})
t.Run("local time invalid second digit", func(t *testing.T) {
testFlow(t, "foo = 00:30:x0.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenError, "invalid second digit in time: x"},
})
})
t.Run("local time invalid second digit", func(t *testing.T) {
testFlow(t, "foo = 00:30:00.F", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenError, "expected at least one digit in time's fraction, not F"},
})
})
t.Run("local date-time invalid minute digit", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:3x:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenError, "invalid minute digit in time: x"},
})
})
t.Run("local date-time invalid hour digit", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T0x:30:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenError, "invalid hour digit in time: x"},
})
})
t.Run("local date-time invalid hour digit", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T00x30:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenError, "time hour/minute separator should be :, not x"},
})
})
t.Run("local date-time invalid minute/second digit", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:30x00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenError, "time minute/second separator should be :, not x"},
})
})
t.Run("local date-time invalid second digit", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:30:x0.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenError, "invalid second digit in time: x"},
})
})
t.Run("local date-time invalid fraction", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:30:00.F", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenError, "expected at least one digit in time's fraction, not F"},
})
})
t.Run("local date-time invalid month-date separator", func(t *testing.T) {
testFlow(t, "foo = 1979-05X27 00:30:00.F", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenError, "expected - to separate month of a date, not X"},
})
})
t.Run("local date-time extra whitespace", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 ", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 19}, tokenEOF, ""},
})
})
t.Run("local date-time extra whitespace", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 ", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 22}, tokenEOF, ""},
})
})
t.Run("offset date-time space separated offset", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:32:00-0x:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00"},
{Position{1, 26}, tokenError, "invalid hour digit in time offset: x"},
})
})
t.Run("offset date-time space separated offset", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:32:00-07x00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00"},
{Position{1, 26}, tokenError, "time offset hour/minute separator should be :, not x"},
})
})
t.Run("offset date-time space separated offset", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:32:00-07:x0", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00"},
{Position{1, 26}, tokenError, "invalid minute digit in time offset: x"},
})
}) })
} }
@@ -657,6 +932,13 @@ func TestMultilineString(t *testing.T) {
{Position{6, 9}, tokenEOF, ""}, {Position{6, 9}, tokenEOF, ""},
}) })
testFlow(t, `foo = """hello world"""`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 10}, tokenString, "hello\tworld"},
{Position{1, 24}, tokenEOF, ""},
})
testFlow(t, "key2 = \"\"\"\nThe quick brown \\\n\n\n fox jumps over \\\n the lazy dog.\"\"\"", []token{ testFlow(t, "key2 = \"\"\"\nThe quick brown \\\n\n\n fox jumps over \\\n the lazy dog.\"\"\"", []token{
{Position{1, 1}, tokenKey, "key2"}, {Position{1, 1}, tokenKey, "key2"},
{Position{1, 6}, tokenEqual, "="}, {Position{1, 6}, tokenEqual, "="},
@@ -694,6 +976,7 @@ func TestUnicodeString(t *testing.T) {
{Position{1, 22}, tokenEOF, ""}, {Position{1, 22}, tokenEOF, ""},
}) })
} }
func TestEscapeInString(t *testing.T) { func TestEscapeInString(t *testing.T) {
testFlow(t, `foo = "\b\f\/"`, []token{ testFlow(t, `foo = "\b\f\/"`, []token{
{Position{1, 1}, tokenKey, "foo"}, {Position{1, 1}, tokenKey, "foo"},
@@ -703,6 +986,15 @@ func TestEscapeInString(t *testing.T) {
}) })
} }
func TestTabInString(t *testing.T) {
testFlow(t, `foo = "hello world"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "hello\tworld"},
{Position{1, 20}, tokenEOF, ""},
})
}
func TestKeyGroupArray(t *testing.T) { func TestKeyGroupArray(t *testing.T) {
testFlow(t, "[[foo]]", []token{ testFlow(t, "[[foo]]", []token{
{Position{1, 1}, tokenDoubleLeftBracket, "[["}, {Position{1, 1}, tokenDoubleLeftBracket, "[["},
@@ -721,6 +1013,15 @@ func TestQuotedKey(t *testing.T) {
}) })
} }
func TestQuotedKeyTab(t *testing.T) {
testFlow(t, "\"num\tber\" = 123", []token{
{Position{1, 1}, tokenKey, "\"num\tber\""},
{Position{1, 11}, tokenEqual, "="},
{Position{1, 13}, tokenInteger, "123"},
{Position{1, 16}, tokenEOF, ""},
})
}
func TestKeyNewline(t *testing.T) { func TestKeyNewline(t *testing.T) {
testFlow(t, "a\n= 4", []token{ testFlow(t, "a\n= 4", []token{
{Position{1, 1}, tokenError, "keys cannot contain new lines"}, {Position{1, 1}, tokenError, "keys cannot contain new lines"},
@@ -750,6 +1051,178 @@ func TestLexUnknownRvalue(t *testing.T) {
}) })
} }
func TestLexInlineTableEmpty(t *testing.T) {
testFlow(t, `foo = {}`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 8}, tokenRightCurlyBrace, "}"},
{Position{1, 9}, tokenEOF, ""},
})
}
func TestLexInlineTableBareKey(t *testing.T) {
testFlow(t, `foo = { bar = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{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 TestLexInlineTableBareKeyInArray(t *testing.T) {
testFlow(t, `foo = [{ -bar_ = "baz" }]`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftBracket, "["},
{Position{1, 8}, tokenLeftCurlyBrace, "{"},
{Position{1, 10}, tokenKey, "-bar_"},
{Position{1, 16}, tokenEqual, "="},
{Position{1, 19}, tokenString, "baz"},
{Position{1, 24}, tokenRightCurlyBrace, "}"},
{Position{1, 25}, tokenRightBracket, "]"},
{Position{1, 26}, tokenEOF, ""},
})
}
func TestLexInlineTableError1(t *testing.T) {
testFlow(t, `foo = { 123 = 0 ]`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "123"},
{Position{1, 13}, tokenEqual, "="},
{Position{1, 15}, tokenInteger, "0"},
{Position{1, 17}, tokenRightBracket, "]"},
{Position{1, 18}, tokenError, "cannot have ']' here"},
})
}
func TestLexInlineTableError2(t *testing.T) {
testFlow(t, `foo = { 123 = 0 }}`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "123"},
{Position{1, 13}, tokenEqual, "="},
{Position{1, 15}, tokenInteger, "0"},
{Position{1, 17}, tokenRightCurlyBrace, "}"},
{Position{1, 18}, tokenRightCurlyBrace, "}"},
{Position{1, 19}, tokenError, "cannot have '}' here"},
})
}
func TestLexInlineTableDottedKey1(t *testing.T) {
testFlow(t, `foo = { a = 0, 123.45abc = 0 }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "a"},
{Position{1, 11}, tokenEqual, "="},
{Position{1, 13}, tokenInteger, "0"},
{Position{1, 14}, tokenComma, ","},
{Position{1, 16}, tokenKey, "123.45abc"},
{Position{1, 26}, tokenEqual, "="},
{Position{1, 28}, tokenInteger, "0"},
{Position{1, 30}, tokenRightCurlyBrace, "}"},
{Position{1, 31}, tokenEOF, ""},
})
}
func TestLexInlineTableDottedKey2(t *testing.T) {
testFlow(t, `foo = { a = 0, '123'.'45abc' = 0 }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "a"},
{Position{1, 11}, tokenEqual, "="},
{Position{1, 13}, tokenInteger, "0"},
{Position{1, 14}, tokenComma, ","},
{Position{1, 16}, tokenKey, "'123'.'45abc'"},
{Position{1, 30}, tokenEqual, "="},
{Position{1, 32}, tokenInteger, "0"},
{Position{1, 34}, tokenRightCurlyBrace, "}"},
{Position{1, 35}, tokenEOF, ""},
})
}
func TestLexInlineTableDottedKey3(t *testing.T) {
testFlow(t, `foo = { a = 0, "123"."45ʎǝʞ" = 0 }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "a"},
{Position{1, 11}, tokenEqual, "="},
{Position{1, 13}, tokenInteger, "0"},
{Position{1, 14}, tokenComma, ","},
{Position{1, 16}, tokenKey, `"123"."45ʎǝʞ"`},
{Position{1, 30}, tokenEqual, "="},
{Position{1, 32}, tokenInteger, "0"},
{Position{1, 34}, tokenRightCurlyBrace, "}"},
{Position{1, 35}, tokenEOF, ""},
})
}
func TestLexInlineTableBareKeyWithComma(t *testing.T) {
testFlow(t, `foo = { -bar1 = "baz", -bar_ = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "-bar1"},
{Position{1, 15}, tokenEqual, "="},
{Position{1, 18}, tokenString, "baz"},
{Position{1, 22}, tokenComma, ","},
{Position{1, 24}, tokenKey, "-bar_"},
{Position{1, 30}, tokenEqual, "="},
{Position{1, 33}, tokenString, "baz"},
{Position{1, 38}, tokenRightCurlyBrace, "}"},
{Position{1, 39}, tokenEOF, ""},
})
}
func TestLexInlineTableBareKeyUnderscore(t *testing.T) {
testFlow(t, `foo = { _bar = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{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) { func BenchmarkLexer(b *testing.B) {
sample := `title = "Hugo: A Fast and Flexible Website Generator" sample := `title = "Hugo: A Fast and Flexible Website Generator"
baseurl = "http://gohugo.io/" baseurl = "http://gohugo.io/"
@@ -767,13 +1240,8 @@ pluralizeListTitles = false
url = "https://github.com/spf13/hugo/releases" url = "https://github.com/spf13/hugo/releases"
weight = -200 weight = -200
` `
rd := strings.NewReader(sample)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
rd.Seek(0, os.SEEK_SET) lexToml([]byte(sample))
ch := lexToml(rd)
for _ = range ch {
}
} }
} }
+281
View File
@@ -0,0 +1,281 @@
// Implementation of TOML's local date/time.
// Copied over from https://github.com/googleapis/google-cloud-go/blob/master/civil/civil.go
// to avoid pulling all the Google dependencies.
//
// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package civil implements types for civil time, a time-zone-independent
// representation of time that follows the rules of the proleptic
// Gregorian calendar with exactly 24-hour days, 60-minute hours, and 60-second
// minutes.
//
// 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 (
"fmt"
"time"
)
// A LocalDate represents a date (year, month, day).
//
// This type does not include location information, and therefore does not
// describe a unique 24-hour timespan.
type LocalDate struct {
Year int // Year (e.g., 2014).
Month time.Month // Month of the year (January = 1, ...).
Day int // Day of the month, starting at 1.
}
// LocalDateOf returns the LocalDate in which a time occurs in that time's location.
func LocalDateOf(t time.Time) LocalDate {
var d LocalDate
d.Year, d.Month, d.Day = t.Date()
return d
}
// ParseLocalDate parses a string in RFC3339 full-date format and returns the date value it represents.
func ParseLocalDate(s string) (LocalDate, error) {
t, err := time.Parse("2006-01-02", s)
if err != nil {
return LocalDate{}, err
}
return LocalDateOf(t), nil
}
// String returns the date in RFC3339 full-date format.
func (d LocalDate) String() string {
return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
}
// IsValid reports whether the date is valid.
func (d LocalDate) IsValid() bool {
return LocalDateOf(d.In(time.UTC)) == d
}
// In returns the time corresponding to time 00:00:00 of the date in the location.
//
// In is always consistent with time.LocalDate, even when time.LocalDate returns a time
// on a different day. For example, if loc is America/Indiana/Vincennes, then both
// time.LocalDate(1955, time.May, 1, 0, 0, 0, 0, loc)
// and
// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}.In(loc)
// return 23:00:00 on April 30, 1955.
//
// In panics if loc is nil.
func (d LocalDate) In(loc *time.Location) time.Time {
return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc)
}
// AddDays returns the date that is n days in the future.
// n can also be negative to go into the past.
func (d LocalDate) AddDays(n int) LocalDate {
return LocalDateOf(d.In(time.UTC).AddDate(0, 0, n))
}
// DaysSince returns the signed number of days between the date and s, not including the end day.
// This is the inverse operation to AddDays.
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)
}
// Before reports whether d1 occurs before d2.
func (d1 LocalDate) Before(d2 LocalDate) bool {
if d1.Year != d2.Year {
return d1.Year < d2.Year
}
if d1.Month != d2.Month {
return d1.Month < d2.Month
}
return d1.Day < d2.Day
}
// After reports whether d1 occurs after d2.
func (d1 LocalDate) After(d2 LocalDate) bool {
return d2.Before(d1)
}
// MarshalText implements the encoding.TextMarshaler interface.
// The output is the result of d.String().
func (d LocalDate) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
// The date is expected to be a string in a format accepted by ParseLocalDate.
func (d *LocalDate) UnmarshalText(data []byte) error {
var err error
*d, err = ParseLocalDate(string(data))
return err
}
// A LocalTime represents a time with nanosecond precision.
//
// This type does not include location information, and therefore does not
// describe a unique moment in time.
//
// This type exists to represent the TIME type in storage-based APIs like BigQuery.
// Most operations on Times are unlikely to be meaningful. Prefer the LocalDateTime type.
type LocalTime struct {
Hour int // The hour of the day in 24-hour format; range [0-23]
Minute int // The minute of the hour; range [0-59]
Second int // The second of the minute; range [0-59]
Nanosecond int // The nanosecond of the second; range [0-999999999]
}
// LocalTimeOf returns the LocalTime representing the time of day in which a time occurs
// in that time's location. It ignores the date.
func LocalTimeOf(t time.Time) LocalTime {
var tm LocalTime
tm.Hour, tm.Minute, tm.Second = t.Clock()
tm.Nanosecond = t.Nanosecond()
return tm
}
// ParseLocalTime parses a string and returns the time value it represents.
// ParseLocalTime accepts an extended form of the RFC3339 partial-time format. After
// the HH:MM:SS part of the string, an optional fractional part may appear,
// consisting of a decimal point followed by one to nine decimal digits.
// (RFC3339 admits only one digit after the decimal point).
func ParseLocalTime(s string) (LocalTime, error) {
t, err := time.Parse("15:04:05.999999999", s)
if err != nil {
return LocalTime{}, err
}
return LocalTimeOf(t), nil
}
// String returns the date in the format described in ParseLocalTime. If Nanoseconds
// is zero, no fractional part will be generated. Otherwise, the result will
// end with a fractional part consisting of a decimal point and nine digits.
func (t LocalTime) String() string {
s := fmt.Sprintf("%02d:%02d:%02d", t.Hour, t.Minute, t.Second)
if t.Nanosecond == 0 {
return s
}
return s + fmt.Sprintf(".%09d", t.Nanosecond)
}
// IsValid reports whether the time is valid.
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
}
// MarshalText implements the encoding.TextMarshaler interface.
// The output is the result of t.String().
func (t LocalTime) MarshalText() ([]byte, error) {
return []byte(t.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
// The time is expected to be a string in a format accepted by ParseLocalTime.
func (t *LocalTime) UnmarshalText(data []byte) error {
var err error
*t, err = ParseLocalTime(string(data))
return err
}
// A LocalDateTime represents a date and time.
//
// This type does not include location information, and therefore does not
// describe a unique moment in time.
type LocalDateTime struct {
Date LocalDate
Time LocalTime
}
// Note: We deliberately do not embed LocalDate into LocalDateTime, to avoid promoting AddDays and Sub.
// LocalDateTimeOf returns the LocalDateTime in which a time occurs in that time's location.
func LocalDateTimeOf(t time.Time) LocalDateTime {
return LocalDateTime{
Date: LocalDateOf(t),
Time: LocalTimeOf(t),
}
}
// ParseLocalDateTime parses a string and returns the LocalDateTime it represents.
// ParseLocalDateTime accepts a variant of the RFC3339 date-time format that omits
// the time offset but includes an optional fractional time, as described in
// ParseLocalTime. Informally, the accepted format is
// YYYY-MM-DDTHH:MM:SS[.FFFFFFFFF]
// where the 'T' may be a lower-case 't'.
func ParseLocalDateTime(s string) (LocalDateTime, error) {
t, err := time.Parse("2006-01-02T15:04:05.999999999", s)
if err != nil {
t, err = time.Parse("2006-01-02t15:04:05.999999999", s)
if err != nil {
return LocalDateTime{}, err
}
}
return LocalDateTimeOf(t), nil
}
// String returns the date in the format described in ParseLocalDate.
func (dt LocalDateTime) String() string {
return dt.Date.String() + "T" + dt.Time.String()
}
// IsValid reports whether the datetime is valid.
func (dt LocalDateTime) IsValid() bool {
return dt.Date.IsValid() && dt.Time.IsValid()
}
// In returns the time corresponding to the LocalDateTime in the given location.
//
// If the time is missing or ambigous at the location, In returns the same
// result as time.LocalDate. For example, if loc is America/Indiana/Vincennes, then
// both
// time.LocalDate(1955, time.May, 1, 0, 30, 0, 0, loc)
// and
// civil.LocalDateTime{
// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}},
// civil.LocalTime{Minute: 30}}.In(loc)
// return 23:30:00 on April 30, 1955.
//
// 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)
}
// Before reports whether dt1 occurs before dt2.
func (dt1 LocalDateTime) Before(dt2 LocalDateTime) bool {
return dt1.In(time.UTC).Before(dt2.In(time.UTC))
}
// After reports whether dt1 occurs after dt2.
func (dt1 LocalDateTime) After(dt2 LocalDateTime) bool {
return dt2.Before(dt1)
}
// MarshalText implements the encoding.TextMarshaler interface.
// The output is the result of dt.String().
func (dt LocalDateTime) MarshalText() ([]byte, error) {
return []byte(dt.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
// 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
}
+446
View File
@@ -0,0 +1,446 @@
// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package toml
import (
"encoding/json"
"reflect"
"testing"
"time"
)
func cmpEqual(x, y interface{}) bool {
return reflect.DeepEqual(x, y)
}
func TestDates(t *testing.T) {
for _, test := range []struct {
date LocalDate
loc *time.Location
wantStr string
wantTime time.Time
}{
{
date: LocalDate{2014, 7, 29},
loc: time.Local,
wantStr: "2014-07-29",
wantTime: time.Date(2014, time.July, 29, 0, 0, 0, 0, time.Local),
},
{
date: LocalDateOf(time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local)),
loc: time.UTC,
wantStr: "2014-08-20",
wantTime: time.Date(2014, 8, 20, 0, 0, 0, 0, time.UTC),
},
{
date: LocalDateOf(time.Date(999, time.January, 26, 0, 0, 0, 0, time.Local)),
loc: time.UTC,
wantStr: "0999-01-26",
wantTime: time.Date(999, 1, 26, 0, 0, 0, 0, time.UTC),
},
} {
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)
}
}
}
func TestDateIsValid(t *testing.T) {
for _, test := range []struct {
date LocalDate
want bool
}{
{LocalDate{2014, 7, 29}, true},
{LocalDate{2000, 2, 29}, true},
{LocalDate{10000, 12, 31}, true},
{LocalDate{1, 1, 1}, true},
{LocalDate{0, 1, 1}, true}, // year zero is OK
{LocalDate{-1, 1, 1}, true}, // negative year is OK
{LocalDate{1, 0, 1}, false},
{LocalDate{1, 1, 0}, false},
{LocalDate{2016, 1, 32}, false},
{LocalDate{2016, 13, 1}, false},
{LocalDate{1, -1, 1}, false},
{LocalDate{1, 1, -1}, false},
} {
got := test.date.IsValid()
if got != test.want {
t.Errorf("%#v: got %t, want %t", test.date, got, test.want)
}
}
}
func TestParseDate(t *testing.T) {
for _, test := range []struct {
str string
want LocalDate // if empty, expect an error
}{
{"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{}},
} {
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{}) {
t.Errorf("Unexpected error %v from ParseLocalDate(%q)", err, test.str)
}
}
}
func TestDateArithmetic(t *testing.T) {
for _, test := range []struct {
desc string
start LocalDate
end LocalDate
days int
}{
{
desc: "zero days noop",
start: LocalDate{2014, 5, 9},
end: LocalDate{2014, 5, 9},
days: 0,
},
{
desc: "crossing a year boundary",
start: LocalDate{2014, 12, 31},
end: LocalDate{2015, 1, 1},
days: 1,
},
{
desc: "negative number of days",
start: LocalDate{2015, 1, 1},
end: LocalDate{2014, 12, 31},
days: -1,
},
{
desc: "full leap year",
start: LocalDate{2004, 1, 1},
end: LocalDate{2005, 1, 1},
days: 366,
},
{
desc: "full non-leap year",
start: LocalDate{2001, 1, 1},
end: LocalDate{2002, 1, 1},
days: 365,
},
{
desc: "crossing a leap second",
start: LocalDate{1972, 6, 30},
end: LocalDate{1972, 7, 1},
days: 1,
},
{
desc: "dates before the unix epoch",
start: LocalDate{101, 1, 1},
end: LocalDate{102, 1, 1},
days: 365,
},
} {
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)
}
}
}
func TestDateBefore(t *testing.T) {
for _, test := range []struct {
d1, d2 LocalDate
want bool
}{
{LocalDate{2016, 12, 31}, LocalDate{2017, 1, 1}, true},
{LocalDate{2016, 1, 1}, LocalDate{2016, 1, 1}, false},
{LocalDate{2016, 12, 30}, LocalDate{2016, 12, 31}, true},
{LocalDate{2016, 1, 30}, LocalDate{2016, 12, 31}, true},
} {
if got := test.d1.Before(test.d2); got != test.want {
t.Errorf("%v.Before(%v): got %t, want %t", test.d1, test.d2, got, test.want)
}
}
}
func TestDateAfter(t *testing.T) {
for _, test := range []struct {
d1, d2 LocalDate
want bool
}{
{LocalDate{2016, 12, 31}, LocalDate{2017, 1, 1}, false},
{LocalDate{2016, 1, 1}, LocalDate{2016, 1, 1}, false},
{LocalDate{2016, 12, 30}, LocalDate{2016, 12, 31}, false},
} {
if got := test.d1.After(test.d2); got != test.want {
t.Errorf("%v.After(%v): got %t, want %t", test.d1, test.d2, got, test.want)
}
}
}
func TestTimeToString(t *testing.T) {
for _, test := range []struct {
str string
time LocalTime
roundTrip bool // ParseLocalTime(str).String() == str?
}{
{"13:26:33", LocalTime{13, 26, 33, 0}, true},
{"01:02:03.000023456", LocalTime{1, 2, 3, 23456}, true},
{"00:00:00.000000001", LocalTime{0, 0, 0, 1}, true},
{"13:26:03.1", LocalTime{13, 26, 3, 100000000}, false},
{"13:26:33.0000003", LocalTime{13, 26, 33, 300}, false},
} {
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 {
t.Errorf("%#v.String() = %q, want %q", test.time, gotStr, test.str)
}
}
}
}
func TestTimeOf(t *testing.T) {
for _, test := range []struct {
time time.Time
want LocalTime
}{
{time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local), LocalTime{15, 8, 43, 1}},
{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC), LocalTime{0, 0, 0, 0}},
} {
if got := LocalTimeOf(test.time); got != test.want {
t.Errorf("LocalTimeOf(%v) = %+v, want %+v", test.time, got, test.want)
}
}
}
func TestTimeIsValid(t *testing.T) {
for _, test := range []struct {
time LocalTime
want bool
}{
{LocalTime{0, 0, 0, 0}, true},
{LocalTime{23, 0, 0, 0}, true},
{LocalTime{23, 59, 59, 999999999}, true},
{LocalTime{24, 59, 59, 999999999}, false},
{LocalTime{23, 60, 59, 999999999}, false},
{LocalTime{23, 59, 60, 999999999}, false},
{LocalTime{23, 59, 59, 1000000000}, false},
{LocalTime{-1, 0, 0, 0}, false},
{LocalTime{0, -1, 0, 0}, false},
{LocalTime{0, 0, -1, 0}, false},
{LocalTime{0, 0, 0, -1}, false},
} {
got := test.time.IsValid()
if got != test.want {
t.Errorf("%#v: got %t, want %t", test.time, got, test.want)
}
}
}
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},
} {
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 {
t.Errorf("%#v.String() = %q, want %q", test.dateTime, gotStr, test.str)
}
}
}
}
func TestParseDateTimeErrors(t *testing.T) {
for _, str := range []string{
"",
"2016-03-22", // just a date
"13:26:33", // just a time
"2016-03-22 13:26:33", // wrong separating character
"2016-03-22T13:26:33x", // extra at end
} {
if _, err := ParseLocalDateTime(str); err == nil {
t.Errorf("ParseLocalDateTime(%q) succeeded, want error", str)
}
}
}
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}}},
} {
if got := LocalDateTimeOf(test.time); got != test.want {
t.Errorf("LocalDateTimeOf(%v) = %+v, want %+v", test.time, got, test.want)
}
}
}
func TestDateTimeIsValid(t *testing.T) {
// No need to be exhaustive here; it's just LocalDate.IsValid && LocalTime.IsValid.
for _, test := range []struct {
dt LocalDateTime
want bool
}{
{LocalDateTime{LocalDate{2016, 3, 20}, LocalTime{0, 0, 0, 0}}, true},
{LocalDateTime{LocalDate{2016, -3, 20}, LocalTime{0, 0, 0, 0}}, false},
{LocalDateTime{LocalDate{2016, 3, 20}, LocalTime{24, 0, 0, 0}}, false},
} {
got := test.dt.IsValid()
if got != test.want {
t.Errorf("%#v: got %t, want %t", test.dt, got, test.want)
}
}
}
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) {
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
}{
{LocalDateTime{d1, t1}, LocalDateTime{d2, t1}, true},
{LocalDateTime{d1, t1}, LocalDateTime{d1, t2}, true},
{LocalDateTime{d2, t1}, LocalDateTime{d1, t1}, false},
{LocalDateTime{d2, t1}, LocalDateTime{d2, t1}, false},
} {
if got := test.dt1.Before(test.dt2); got != test.want {
t.Errorf("%v.Before(%v): got %t, want %t", test.dt1, test.dt2, got, test.want)
}
}
}
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
}{
{LocalDateTime{d1, t1}, LocalDateTime{d2, t1}, false},
{LocalDateTime{d1, t1}, LocalDateTime{d1, t2}, false},
{LocalDateTime{d2, t1}, LocalDateTime{d1, t1}, true},
{LocalDateTime{d2, t1}, LocalDateTime{d2, t1}, false},
} {
if got := test.dt1.After(test.dt2); got != test.want {
t.Errorf("%v.After(%v): got %t, want %t", test.dt1, test.dt2, got, test.want)
}
}
}
func TestMarshalJSON(t *testing.T) {
for _, test := range []struct {
value interface{}
want string
}{
{LocalDate{1987, 4, 15}, `"1987-04-15"`},
{LocalTime{18, 54, 2, 0}, `"18:54:02"`},
{LocalDateTime{LocalDate{1987, 4, 15}, LocalTime{18, 54, 2, 0}}, `"1987-04-15T18:54:02"`},
} {
bgot, err := json.Marshal(test.value)
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)
}
}
}
func TestUnmarshalJSON(t *testing.T) {
var d LocalDate
var tm LocalTime
var dt LocalDateTime
for _, test := range []struct {
data string
ptr interface{}
want interface{}
}{
{`"1987-04-15"`, &d, &LocalDate{1987, 4, 15}},
{`"1987-04-\u0031\u0035"`, &d, &LocalDate{1987, 4, 15}},
{`"18:54:02"`, &tm, &LocalTime{18, 54, 2, 0}},
{`"1987-04-15T18:54:02"`, &dt, &LocalDateTime{LocalDate{1987, 4, 15}, LocalTime{18, 54, 2, 0}}},
} {
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"`,
`19870415`, // a JSON number
`11987-04-15x`, // not a JSON string
} {
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)
}
}
}
+1013 -196
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
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"
+3530 -69
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -4,6 +4,7 @@ title = "TOML Marshal Testing"
bool = true bool = true
date = 1979-05-27T07:32:00Z date = 1979-05-27T07:32:00Z
float = 123.4 float = 123.4
float64 = 123.456782132399
int = 5000 int = 5000
string = "Bite me" string = "Bite me"
uint = 5001 uint = 5001
+177 -62
View File
@@ -5,17 +5,17 @@ package toml
import ( import (
"errors" "errors"
"fmt" "fmt"
"math"
"reflect" "reflect"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
type tomlParser struct { type tomlParser struct {
flow chan token flowIdx int
flow []token
tree *Tree tree *Tree
tokensBuffer []token
currentTable []string currentTable []string
seenTableKeys []string seenTableKeys []string
} }
@@ -34,16 +34,10 @@ func (p *tomlParser) run() {
} }
func (p *tomlParser) peek() *token { func (p *tomlParser) peek() *token {
if len(p.tokensBuffer) != 0 { if p.flowIdx >= len(p.flow) {
return &(p.tokensBuffer[0])
}
tok, ok := <-p.flow
if !ok {
return nil return nil
} }
p.tokensBuffer = append(p.tokensBuffer, tok) return &p.flow[p.flowIdx]
return &tok
} }
func (p *tomlParser) assume(typ tokenType) { func (p *tomlParser) assume(typ tokenType) {
@@ -57,16 +51,12 @@ func (p *tomlParser) assume(typ tokenType) {
} }
func (p *tomlParser) getToken() *token { func (p *tomlParser) getToken() *token {
if len(p.tokensBuffer) != 0 { tok := p.peek()
tok := p.tokensBuffer[0] if tok == nil {
p.tokensBuffer = p.tokensBuffer[1:]
return &tok
}
tok, ok := <-p.flow
if !ok {
return nil return nil
} }
return &tok p.flowIdx++
return tok
} }
func (p *tomlParser) parseStart() tomlParserStateFn { func (p *tomlParser) parseStart() tomlParserStateFn {
@@ -86,8 +76,10 @@ func (p *tomlParser) parseStart() tomlParserStateFn {
return p.parseAssign return p.parseAssign
case tokenEOF: case tokenEOF:
return nil return nil
case tokenError:
p.raiseError(tok, "parsing error: %s", tok.String())
default: default:
p.raiseError(tok, "unexpected token") p.raiseError(tok, "unexpected token %s", tok.typ)
} }
return nil return nil
} }
@@ -165,6 +157,11 @@ func (p *tomlParser) parseGroup() tomlParserStateFn {
if err := p.tree.createSubTree(keys, startToken.Position); err != nil { if err := p.tree.createSubTree(keys, startToken.Position); err != nil {
p.raiseError(key, "%s", err) p.raiseError(key, "%s", err)
} }
destTree := p.tree.GetPath(keys)
if target, ok := destTree.(*Tree); ok && target != nil && target.inline {
p.raiseError(key, "could not re-define exist inline table or its sub-table : %s",
strings.Join(keys, "."))
}
p.assume(tokenRightBracket) p.assume(tokenRightBracket)
p.currentTable = keys p.currentTable = keys
return p.parseStart return p.parseStart
@@ -174,6 +171,11 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
key := p.getToken() key := p.getToken()
p.assume(tokenEqual) p.assume(tokenEqual)
parsedKey, err := parseKey(key.val)
if err != nil {
p.raiseError(key, "invalid key: %s", err.Error())
}
value := p.parseRvalue() value := p.parseRvalue()
var tableKey []string var tableKey []string
if len(p.currentTable) > 0 { if len(p.currentTable) > 0 {
@@ -182,6 +184,9 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
tableKey = []string{} tableKey = []string{}
} }
prefixKey := parsedKey[0 : len(parsedKey)-1]
tableKey = append(tableKey, prefixKey...)
// find the table to assign, looking out for arrays of tables // find the table to assign, looking out for arrays of tables
var targetNode *Tree var targetNode *Tree
switch node := p.tree.GetPath(tableKey).(type) { switch node := p.tree.GetPath(tableKey).(type) {
@@ -189,20 +194,24 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
targetNode = node[len(node)-1] targetNode = node[len(node)-1]
case *Tree: case *Tree:
targetNode = node targetNode = node
case nil:
// create intermediate
if err := p.tree.createSubTree(tableKey, key.Position); err != nil {
p.raiseError(key, "could not create intermediate group: %s", err)
}
targetNode = p.tree.GetPath(tableKey).(*Tree)
default: default:
p.raiseError(key, "Unknown table type for path: %s", p.raiseError(key, "Unknown table type for path: %s",
strings.Join(tableKey, ".")) strings.Join(tableKey, "."))
} }
if targetNode.inline {
p.raiseError(key, "could not add key or sub-table to exist inline table or its sub-table : %s",
strings.Join(tableKey, "."))
}
// assign value to the found table // assign value to the found table
keyVals, err := parseKey(key.val) keyVal := parsedKey[len(parsedKey)-1]
if err != nil {
p.raiseError(key, "%s", err)
}
if len(keyVals) != 1 {
p.raiseError(key, "Invalid key")
}
keyVal := keyVals[0]
localKey := []string{keyVal} localKey := []string{keyVal}
finalKey := append(tableKey, keyVal) finalKey := append(tableKey, keyVal)
if targetNode.GetPath(localKey) != nil { if targetNode.GetPath(localKey) != nil {
@@ -215,20 +224,51 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
case *Tree, []*Tree: case *Tree, []*Tree:
toInsert = value toInsert = value
default: default:
toInsert = &tomlValue{value, key.Position} toInsert = &tomlValue{value: value, position: key.Position}
} }
targetNode.values[keyVal] = toInsert targetNode.values[keyVal] = toInsert
return p.parseStart return p.parseStart
} }
var numberUnderscoreInvalidRegexp *regexp.Regexp var errInvalidUnderscore = errors.New("invalid use of _ in number")
func cleanupNumberToken(value string) (string, error) { func numberContainsInvalidUnderscore(value string) error {
if numberUnderscoreInvalidRegexp.MatchString(value) { // For large numbers, you may use underscores between digits to enhance
return "", errors.New("invalid use of _ in number") // readability. Each underscore must be surrounded by at least one digit on
// each side.
hasBefore := false
for idx, r := range value {
if r == '_' {
if !hasBefore || idx+1 >= len(value) {
// can't end with an underscore
return errInvalidUnderscore
}
}
hasBefore = isDigit(r)
} }
return nil
}
var errInvalidUnderscoreHex = errors.New("invalid use of _ in hex number")
func hexNumberContainsInvalidUnderscore(value string) error {
hasBefore := false
for idx, r := range value {
if r == '_' {
if !hasBefore || idx+1 >= len(value) {
// can't end with an underscore
return errInvalidUnderscoreHex
}
}
hasBefore = isHexDigit(r)
}
return nil
}
func cleanupNumberToken(value string) string {
cleanedVal := strings.Replace(value, "_", "", -1) cleanedVal := strings.Replace(value, "_", "", -1)
return cleanedVal, nil return cleanedVal
} }
func (p *tomlParser) parseRvalue() interface{} { func (p *tomlParser) parseRvalue() interface{} {
@@ -244,28 +284,100 @@ func (p *tomlParser) parseRvalue() interface{} {
return true return true
case tokenFalse: case tokenFalse:
return false return false
case tokenInteger: case tokenInf:
cleanedVal, err := cleanupNumberToken(tok.val) if tok.val[0] == '-' {
if err != nil { return math.Inf(-1)
p.raiseError(tok, "%s", err) }
return math.Inf(1)
case tokenNan:
return math.NaN()
case tokenInteger:
cleanedVal := cleanupNumberToken(tok.val)
var err error
var val int64
if len(cleanedVal) >= 3 && cleanedVal[0] == '0' {
switch cleanedVal[1] {
case 'x':
err = hexNumberContainsInvalidUnderscore(tok.val)
if err != nil {
p.raiseError(tok, "%s", err)
}
val, err = strconv.ParseInt(cleanedVal[2:], 16, 64)
case 'o':
err = numberContainsInvalidUnderscore(tok.val)
if err != nil {
p.raiseError(tok, "%s", err)
}
val, err = strconv.ParseInt(cleanedVal[2:], 8, 64)
case 'b':
err = numberContainsInvalidUnderscore(tok.val)
if err != nil {
p.raiseError(tok, "%s", err)
}
val, err = strconv.ParseInt(cleanedVal[2:], 2, 64)
default:
panic("invalid base") // the lexer should catch this first
}
} else {
err = numberContainsInvalidUnderscore(tok.val)
if err != nil {
p.raiseError(tok, "%s", err)
}
val, err = strconv.ParseInt(cleanedVal, 10, 64)
} }
val, err := strconv.ParseInt(cleanedVal, 10, 64)
if err != nil { if err != nil {
p.raiseError(tok, "%s", err) p.raiseError(tok, "%s", err)
} }
return val return val
case tokenFloat: case tokenFloat:
cleanedVal, err := cleanupNumberToken(tok.val) err := numberContainsInvalidUnderscore(tok.val)
if err != nil { if err != nil {
p.raiseError(tok, "%s", err) p.raiseError(tok, "%s", err)
} }
cleanedVal := cleanupNumberToken(tok.val)
val, err := strconv.ParseFloat(cleanedVal, 64) val, err := strconv.ParseFloat(cleanedVal, 64)
if err != nil { if err != nil {
p.raiseError(tok, "%s", err) p.raiseError(tok, "%s", err)
} }
return val return val
case tokenDate: case tokenLocalTime:
val, err := time.ParseInLocation(time.RFC3339Nano, tok.val, time.UTC) val, err := ParseLocalTime(tok.val)
if err != nil {
p.raiseError(tok, "%s", err)
}
return val
case tokenLocalDate:
// a local date may be followed by:
// * nothing: this is a local date
// * a local time: this is a local date-time
next := p.peek()
if next == nil || next.typ != tokenLocalTime {
val, err := ParseLocalDate(tok.val)
if err != nil {
p.raiseError(tok, "%s", err)
}
return val
}
localDate := tok
localTime := p.getToken()
next = p.peek()
if next == nil || next.typ != tokenTimeOffset {
v := localDate.val + "T" + localTime.val
val, err := ParseLocalDateTime(v)
if err != nil {
p.raiseError(tok, "%s", err)
}
return val
}
offset := p.getToken()
layout := time.RFC3339Nano
v := localDate.val + "T" + localTime.val + offset.val
val, err := time.ParseInLocation(layout, v, time.UTC)
if err != nil { if err != nil {
p.raiseError(tok, "%s", err) p.raiseError(tok, "%s", err)
} }
@@ -278,10 +390,10 @@ func (p *tomlParser) parseRvalue() interface{} {
p.raiseError(tok, "cannot have multiple equals for the same key") p.raiseError(tok, "cannot have multiple equals for the same key")
case tokenError: case tokenError:
p.raiseError(tok, "%s", tok) p.raiseError(tok, "%s", tok)
default:
panic(fmt.Errorf("unhandled token: %v", tok))
} }
p.raiseError(tok, "never reached")
return nil return nil
} }
@@ -302,36 +414,40 @@ Loop:
case tokenRightCurlyBrace: case tokenRightCurlyBrace:
p.getToken() p.getToken()
break Loop break Loop
case tokenKey: case tokenKey, tokenInteger, tokenString:
if !tokenIsComma(previous) && previous != nil { if !tokenIsComma(previous) && previous != nil {
p.raiseError(follow, "comma expected between fields in inline table") p.raiseError(follow, "comma expected between fields in inline table")
} }
key := p.getToken() key := p.getToken()
p.assume(tokenEqual) p.assume(tokenEqual)
value := p.parseRvalue()
tree.Set(key.val, value) parsedKey, err := parseKey(key.val)
case tokenComma: if err != nil {
if previous == nil { p.raiseError(key, "invalid key: %s", err)
p.raiseError(follow, "inline table cannot start with a comma")
} }
value := p.parseRvalue()
tree.SetPath(parsedKey, value)
case tokenComma:
if tokenIsComma(previous) { if tokenIsComma(previous) {
p.raiseError(follow, "need field between two commas in inline table") p.raiseError(follow, "need field between two commas in inline table")
} }
p.getToken() p.getToken()
default: default:
p.raiseError(follow, "unexpected token type in inline table: %s", follow.typ.String()) p.raiseError(follow, "unexpected token type in inline table: %s", follow.String())
} }
previous = follow previous = follow
} }
if tokenIsComma(previous) { if tokenIsComma(previous) {
p.raiseError(previous, "trailing comma at the end of inline table") p.raiseError(previous, "trailing comma at the end of inline table")
} }
tree.inline = true
return tree return tree
} }
func (p *tomlParser) parseArray() interface{} { func (p *tomlParser) parseArray() interface{} {
var array []interface{} var array []interface{}
arrayType := reflect.TypeOf(nil) arrayType := reflect.TypeOf(newTree())
for { for {
follow := p.peek() follow := p.peek()
if follow == nil || follow.typ == tokenEOF { if follow == nil || follow.typ == tokenEOF {
@@ -342,11 +458,8 @@ func (p *tomlParser) parseArray() interface{} {
break break
} }
val := p.parseRvalue() val := p.parseRvalue()
if arrayType == nil {
arrayType = reflect.TypeOf(val)
}
if reflect.TypeOf(val) != arrayType { if reflect.TypeOf(val) != arrayType {
p.raiseError(follow, "mixed types in array") arrayType = nil
} }
array = append(array, val) array = append(array, val)
follow = p.peek() follow = p.peek()
@@ -360,6 +473,12 @@ func (p *tomlParser) parseArray() interface{} {
p.getToken() p.getToken()
} }
} }
// if the array is a mixed-type array or its length is 0,
// don't convert it to a table array
if len(array) <= 0 {
arrayType = nil
}
// An array of Trees is actually an array of inline // An array of Trees is actually an array of inline
// tables, which is a shorthand for a table array. If the // tables, which is a shorthand for a table array. If the
// array was not converted from []interface{} to []*Tree, // array was not converted from []interface{} to []*Tree,
@@ -374,20 +493,16 @@ func (p *tomlParser) parseArray() interface{} {
return array return array
} }
func parseToml(flow chan token) *Tree { func parseToml(flow []token) *Tree {
result := newTree() result := newTree()
result.position = Position{1, 1} result.position = Position{1, 1}
parser := &tomlParser{ parser := &tomlParser{
flowIdx: 0,
flow: flow, flow: flow,
tree: result, tree: result,
tokensBuffer: make([]token, 0),
currentTable: make([]string, 0), currentTable: make([]string, 0),
seenTableKeys: make([]string, 0), seenTableKeys: make([]string, 0),
} }
parser.run() parser.run()
return result return result
} }
func init() {
numberUnderscoreInvalidRegexp = regexp.MustCompile(`([^\d]_|_[^\d]|_$|^_)`)
}
+406 -27
View File
@@ -2,11 +2,10 @@ package toml
import ( import (
"fmt" "fmt"
"math"
"reflect" "reflect"
"testing" "testing"
"time" "time"
"github.com/davecgh/go-spew/spew"
) )
func assertSubTree(t *testing.T, path []string, tree *Tree, err error, ref map[string]interface{}) { func assertSubTree(t *testing.T, path []string, tree *Tree, err error, ref map[string]interface{}) {
@@ -38,7 +37,7 @@ func assertSubTree(t *testing.T, path []string, tree *Tree, err error, ref map[s
} }
func assertTree(t *testing.T, tree *Tree, err error, ref map[string]interface{}) { func assertTree(t *testing.T, tree *Tree, err error, ref map[string]interface{}) {
t.Log("Asserting tree:\n", spew.Sdump(tree)) t.Logf("Asserting tree:\n (%T)(%p)(%+v)", tree, tree, tree)
assertSubTree(t, []string{}, tree, err, ref) assertSubTree(t, []string{}, tree, err, ref)
t.Log("Finished tree assertion.") t.Log("Finished tree assertion.")
} }
@@ -72,6 +71,17 @@ func TestNumberInKey(t *testing.T) {
}) })
} }
func TestIncorrectKeyExtraSquareBracket(t *testing.T) {
_, err := Load(`[a]b]
zyx = 42`)
if err == nil {
t.Error("Error should have been returned.")
}
if err.Error() != "(1, 4): parsing error: keys cannot contain ] character" {
t.Error("Bad error message:", err.Error())
}
}
func TestSimpleNumbers(t *testing.T) { func TestSimpleNumbers(t *testing.T) {
tree, err := Load("a = +42\nb = -21\nc = +4.2\nd = -2.1") tree, err := Load("a = +42\nb = -21\nc = +4.2\nd = -2.1")
assertTree(t, tree, err, map[string]interface{}{ assertTree(t, tree, err, map[string]interface{}{
@@ -82,6 +92,78 @@ func TestSimpleNumbers(t *testing.T) {
}) })
} }
func TestSpecialFloats(t *testing.T) {
tree, err := Load(`
normalinf = inf
plusinf = +inf
minusinf = -inf
normalnan = nan
plusnan = +nan
minusnan = -nan
`)
assertTree(t, tree, err, map[string]interface{}{
"normalinf": math.Inf(1),
"plusinf": math.Inf(1),
"minusinf": math.Inf(-1),
"normalnan": math.NaN(),
"plusnan": math.NaN(),
"minusnan": math.NaN(),
})
}
func TestHexIntegers(t *testing.T) {
tree, err := Load(`a = 0xDEADBEEF`)
assertTree(t, tree, err, map[string]interface{}{"a": int64(3735928559)})
tree, err = Load(`a = 0xdeadbeef`)
assertTree(t, tree, err, map[string]interface{}{"a": int64(3735928559)})
tree, err = Load(`a = 0xdead_beef`)
assertTree(t, tree, err, map[string]interface{}{"a": int64(3735928559)})
_, err = Load(`a = 0x_1`)
if err.Error() != "(1, 5): invalid use of _ in hex number" {
t.Error("Bad error message:", err.Error())
}
}
func TestOctIntegers(t *testing.T) {
tree, err := Load(`a = 0o01234567`)
assertTree(t, tree, err, map[string]interface{}{"a": int64(342391)})
tree, err = Load(`a = 0o755`)
assertTree(t, tree, err, map[string]interface{}{"a": int64(493)})
_, err = Load(`a = 0o_1`)
if err.Error() != "(1, 5): invalid use of _ in number" {
t.Error("Bad error message:", err.Error())
}
}
func TestBinIntegers(t *testing.T) {
tree, err := Load(`a = 0b11010110`)
assertTree(t, tree, err, map[string]interface{}{"a": int64(214)})
_, err = Load(`a = 0b_1`)
if err.Error() != "(1, 5): invalid use of _ in number" {
t.Error("Bad error message:", err.Error())
}
}
func TestBadIntegerBase(t *testing.T) {
_, err := Load(`a = 0k1`)
if err.Error() != "(1, 5): unknown number base: k. possible options are x (hex) o (octal) b (binary)" {
t.Error("Error should have been returned.")
}
}
func TestIntegerNoDigit(t *testing.T) {
_, err := Load(`a = 0b`)
if err.Error() != "(1, 5): number needs at least one digit" {
t.Error("Bad error message:", err.Error())
}
}
func TestNumbersWithUnderscores(t *testing.T) { func TestNumbersWithUnderscores(t *testing.T) {
tree, err := Load("a = 1_000") tree, err := Load("a = 1_000")
assertTree(t, tree, err, map[string]interface{}{ assertTree(t, tree, err, map[string]interface{}{
@@ -113,7 +195,7 @@ func TestFloatsWithExponents(t *testing.T) {
tree, err := Load("a = 5e+22\nb = 5E+22\nc = -5e+22\nd = -5e-22\ne = 6.626e-34") tree, err := Load("a = 5e+22\nb = 5E+22\nc = -5e+22\nd = -5e-22\ne = 6.626e-34")
assertTree(t, tree, err, map[string]interface{}{ assertTree(t, tree, err, map[string]interface{}{
"a": float64(5e+22), "a": float64(5e+22),
"b": float64(5E+22), "b": float64(5e+22),
"c": float64(-5e+22), "c": float64(-5e+22),
"d": float64(-5e-22), "d": float64(-5e-22),
"e": float64(6.626e-34), "e": float64(6.626e-34),
@@ -141,6 +223,107 @@ func TestDateNano(t *testing.T) {
}) })
} }
func TestLocalDateTime(t *testing.T) {
tree, err := Load("a = 1979-05-27T07:32:00")
assertTree(t, tree, err, map[string]interface{}{
"a": LocalDateTime{
Date: LocalDate{
Year: 1979,
Month: 5,
Day: 27,
},
Time: LocalTime{
Hour: 7,
Minute: 32,
Second: 0,
Nanosecond: 0,
},
},
})
}
func TestLocalDateTimeNano(t *testing.T) {
tree, err := Load("a = 1979-05-27T07:32:00.999999")
assertTree(t, tree, err, map[string]interface{}{
"a": LocalDateTime{
Date: LocalDate{
Year: 1979,
Month: 5,
Day: 27,
},
Time: LocalTime{
Hour: 7,
Minute: 32,
Second: 0,
Nanosecond: 999999000,
},
},
})
}
func TestLocalDate(t *testing.T) {
tree, err := Load("a = 1979-05-27")
assertTree(t, tree, err, map[string]interface{}{
"a": LocalDate{
Year: 1979,
Month: 5,
Day: 27,
},
})
}
func TestLocalDateError(t *testing.T) {
_, err := Load("a = 2020-09-31")
if err == nil {
t.Fatalf("should error")
}
}
func TestLocalTimeError(t *testing.T) {
_, err := Load("a = 07:99:00")
if err == nil {
t.Fatalf("should error")
}
}
func TestLocalDateTimeError(t *testing.T) {
_, err := Load("a = 2020-09-31T07:99:00")
if err == nil {
t.Fatalf("should error")
}
}
func TestDateTimeOffsetError(t *testing.T) {
_, err := Load("a = 2020-09-31T07:99:00Z")
if err == nil {
t.Fatalf("should error")
}
}
func TestLocalTime(t *testing.T) {
tree, err := Load("a = 07:32:00")
assertTree(t, tree, err, map[string]interface{}{
"a": LocalTime{
Hour: 7,
Minute: 32,
Second: 0,
Nanosecond: 0,
},
})
}
func TestLocalTimeNano(t *testing.T) {
tree, err := Load("a = 00:32:00.999999")
assertTree(t, tree, err, map[string]interface{}{
"a": LocalTime{
Hour: 0,
Minute: 32,
Second: 0,
Nanosecond: 999999000,
},
})
}
func TestSimpleString(t *testing.T) { func TestSimpleString(t *testing.T) {
tree, err := Load("a = \"hello world\"") tree, err := Load("a = \"hello world\"")
assertTree(t, tree, err, map[string]interface{}{ assertTree(t, tree, err, map[string]interface{}{
@@ -155,6 +338,36 @@ func TestSpaceKey(t *testing.T) {
}) })
} }
func TestDoubleQuotedKey(t *testing.T) {
tree, err := Load(`
"key" = "a"
"\t" = "b"
"\U0001F914" = "c"
"\u2764" = "d"
`)
assertTree(t, tree, err, map[string]interface{}{
"key": "a",
"\t": "b",
"\U0001F914": "c",
"\u2764": "d",
})
}
func TestSingleQuotedKey(t *testing.T) {
tree, err := Load(`
'key' = "a"
'\t' = "b"
'\U0001F914' = "c"
'\u2764' = "d"
`)
assertTree(t, tree, err, map[string]interface{}{
`key`: "a",
`\t`: "b",
`\U0001F914`: "c",
`\u2764`: "d",
})
}
func TestStringEscapables(t *testing.T) { func TestStringEscapables(t *testing.T) {
tree, err := Load("a = \"a \\n b\"") tree, err := Load("a = \"a \\n b\"")
assertTree(t, tree, err, map[string]interface{}{ assertTree(t, tree, err, map[string]interface{}{
@@ -301,18 +514,6 @@ func TestNestedEmptyArrays(t *testing.T) {
}) })
} }
func TestArrayMixedTypes(t *testing.T) {
_, err := Load("a = [42, 16.0]")
if err.Error() != "(1, 10): mixed types in array" {
t.Error("Bad error message:", err.Error())
}
_, err = Load("a = [42, \"hello\"]")
if err.Error() != "(1, 11): mixed types in array" {
t.Error("Bad error message:", err.Error())
}
}
func TestArrayNestedStrings(t *testing.T) { func TestArrayNestedStrings(t *testing.T) {
tree, err := Load("data = [ [\"gamma\", \"delta\"], [\"Foo\"] ]") tree, err := Load("data = [ [\"gamma\", \"delta\"], [\"Foo\"] ]")
assertTree(t, tree, err, map[string]interface{}{ assertTree(t, tree, err, map[string]interface{}{
@@ -396,6 +597,39 @@ func TestDoubleInlineGroup(t *testing.T) {
}) })
} }
func TestNestedInlineGroup(t *testing.T) {
tree, err := Load("out = {block0 = {x = 99, y = 100}, block1 = {p = \"999\", q = \"1000\"}}")
assertTree(t, tree, err, map[string]interface{}{
"out": map[string]interface{}{
"block0": map[string]interface{}{
"x": int64(99),
"y": int64(100),
},
"block1": map[string]interface{}{
"p": "999",
"q": "1000",
},
},
})
}
func TestArrayInNestedInlineGroup(t *testing.T) {
tree, err := Load(`image = {name = "xxx", palette = {id = 100, colors = ["red", "blue", "green"]}}`)
assertTree(t, tree, err, map[string]interface{}{
"image": map[string]interface{}{
"name": "xxx",
"palette": map[string]interface{}{
"id": int64(100),
"colors": []string{
"red",
"blue",
"green",
},
},
},
})
}
func TestExampleInlineGroup(t *testing.T) { func TestExampleInlineGroup(t *testing.T) {
tree, err := Load(`name = { first = "Tom", last = "Preston-Werner" } tree, err := Load(`name = { first = "Tom", last = "Preston-Werner" }
point = { x = 1, y = 2 }`) point = { x = 1, y = 2 }`)
@@ -411,6 +645,33 @@ point = { x = 1, y = 2 }`)
}) })
} }
func TestInlineGroupBareKeysUnderscore(t *testing.T) {
tree, err := Load(`foo = { _bar = "buz" }`)
assertTree(t, tree, err, map[string]interface{}{
"foo": map[string]interface{}{
"_bar": "buz",
},
})
}
func TestInlineGroupBareKeysDash(t *testing.T) {
tree, err := Load(`foo = { -bar = "buz" }`)
assertTree(t, tree, err, map[string]interface{}{
"foo": map[string]interface{}{
"-bar": "buz",
},
})
}
func TestInlineGroupKeyQuoted(t *testing.T) {
tree, err := Load(`foo = { "bar" = "buz" }`)
assertTree(t, tree, err, map[string]interface{}{
"foo": map[string]interface{}{
"bar": "buz",
},
})
}
func TestExampleInlineGroupInArray(t *testing.T) { func TestExampleInlineGroupInArray(t *testing.T) {
tree, err := Load(`points = [{ x = 1, y = 2 }]`) tree, err := Load(`points = [{ x = 1, y = 2 }]`)
assertTree(t, tree, err, map[string]interface{}{ assertTree(t, tree, err, map[string]interface{}{
@@ -432,21 +693,56 @@ func TestInlineTableUnterminated(t *testing.T) {
func TestInlineTableCommaExpected(t *testing.T) { func TestInlineTableCommaExpected(t *testing.T) {
_, err := Load("foo = {hello = 53 test = foo}") _, err := Load("foo = {hello = 53 test = foo}")
if err.Error() != "(1, 19): comma expected between fields in inline table" { if err.Error() != "(1, 19): unexpected token type in inline table: no value can start with t" {
t.Error("Bad error message:", err.Error()) t.Error("Bad error message:", err.Error())
} }
} }
func TestInlineTableCommaStart(t *testing.T) { func TestInlineTableCommaStart(t *testing.T) {
_, err := Load("foo = {, hello = 53}") _, err := Load("foo = {, hello = 53}")
if err.Error() != "(1, 8): inline table cannot start with a comma" { if err.Error() != "(1, 8): unexpected token type in inline table: keys cannot contain , character" {
t.Error("Bad error message:", err.Error()) t.Error("Bad error message:", err.Error())
} }
} }
func TestInlineTableDoubleComma(t *testing.T) { func TestInlineTableDoubleComma(t *testing.T) {
_, err := Load("foo = {hello = 53,, foo = 17}") _, err := Load("foo = {hello = 53,, foo = 17}")
if err.Error() != "(1, 19): need field between two commas in inline table" { if err.Error() != "(1, 19): unexpected token type in inline table: keys cannot contain , character" {
t.Error("Bad error message:", err.Error())
}
}
func TestInlineTableTrailingComma(t *testing.T) {
_, err := Load("foo = {hello = 53, foo = 17,}")
if err.Error() != "(1, 28): trailing comma at the end of inline table" {
t.Error("Bad error message:", err.Error())
}
}
func TestAddKeyToInlineTable(t *testing.T) {
_, err := Load("type = { name = \"Nail\" }\ntype.edible = false")
if err.Error() != "(2, 1): could not add key or sub-table to exist inline table or its sub-table : type" {
t.Error("Bad error message:", err.Error())
}
}
func TestAddSubTableToInlineTable(t *testing.T) {
_, err := Load("a = { b = \"c\" }\na.d.e = \"f\"")
if err.Error() != "(2, 1): could not add key or sub-table to exist inline table or its sub-table : a.d" {
t.Error("Bad error message:", err.Error())
}
}
func TestAddKeyToSubTableOfInlineTable(t *testing.T) {
_, err := Load("a = { b = { c = \"d\" } }\na.b.e = \"f\"")
if err.Error() != "(2, 1): could not add key or sub-table to exist inline table or its sub-table : a.b" {
t.Error("Bad error message:", err.Error())
}
}
func TestReDefineInlineTable(t *testing.T) {
_, err := Load("a = { b = \"c\" }\n[a]\n d = \"e\"")
if err.Error() != "(2, 2): could not re-define exist inline table or its sub-table : a" {
t.Error("Bad error message:", err.Error()) t.Error("Bad error message:", err.Error())
} }
} }
@@ -467,7 +763,7 @@ func TestDuplicateKeys(t *testing.T) {
func TestEmptyIntermediateTable(t *testing.T) { func TestEmptyIntermediateTable(t *testing.T) {
_, err := Load("[foo..bar]") _, err := Load("[foo..bar]")
if err.Error() != "(1, 2): invalid table array key: empty table key" { if err.Error() != "(1, 2): invalid table array key: expecting key part after dot" {
t.Error("Bad error message:", err.Error()) t.Error("Bad error message:", err.Error())
} }
} }
@@ -538,6 +834,7 @@ func TestParseFile(t *testing.T) {
[]string{"gamma", "delta"}, []string{"gamma", "delta"},
[]int64{1, 2}, []int64{1, 2},
}, },
"score": 4e-08,
}, },
}) })
} }
@@ -574,6 +871,7 @@ func TestParseFileCRLF(t *testing.T) {
[]string{"gamma", "delta"}, []string{"gamma", "delta"},
[]int64{1, 2}, []int64{1, 2},
}, },
"score": 4e-08,
}, },
}) })
} }
@@ -642,17 +940,15 @@ func TestTomlValueStringRepresentation(t *testing.T) {
{int64(12345), "12345"}, {int64(12345), "12345"},
{uint64(50), "50"}, {uint64(50), "50"},
{float64(123.45), "123.45"}, {float64(123.45), "123.45"},
{bool(true), "true"}, {true, "true"},
{"hello world", "\"hello world\""}, {"hello world", "\"hello world\""},
{"\b\t\n\f\r\"\\", "\"\\b\\t\\n\\f\\r\\\"\\\\\""}, {"\b\t\n\f\r\"\\", "\"\\b\\t\\n\\f\\r\\\"\\\\\""},
{"\x05", "\"\\u0005\""}, {"\x05", "\"\\u0005\""},
{time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC), {time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC), "1979-05-27T07:32:00Z"},
"1979-05-27T07:32:00Z"}, {[]interface{}{"gamma", "delta"}, "[\"gamma\", \"delta\"]"},
{[]interface{}{"gamma", "delta"},
"[\"gamma\",\"delta\"]"},
{nil, ""}, {nil, ""},
} { } {
result, err := tomlValueStringRepresentation(item.Value) result, err := tomlValueStringRepresentation(item.Value, "", "", OrderAlphabetical, false)
if err != nil { if err != nil {
t.Errorf("Test %d - unexpected error: %s", idx, err) t.Errorf("Test %d - unexpected error: %s", idx, err)
} }
@@ -779,7 +1075,90 @@ func TestInvalidFloatParsing(t *testing.T) {
} }
_, err = Load("a=_1_2") _, err = Load("a=_1_2")
if err.Error() != "(1, 3): cannot start number with underscore" { if err.Error() != "(1, 3): no value can start with _" {
t.Error("Bad error message:", err.Error()) t.Error("Bad error message:", err.Error())
} }
} }
func TestMapKeyIsNum(t *testing.T) {
_, err := Load("table={2018=1,2019=2}")
if err != nil {
t.Error("should be passed")
}
_, err = Load(`table={"2018"=1,"2019"=2}`)
if err != nil {
t.Error("should be passed")
}
}
func TestInvalidKeyInlineTable(t *testing.T) {
_, err := Load("table={invalid..key = 1}")
if err.Error() != "(1, 8): invalid key: expecting key part after dot" {
t.Error("Bad error message:", err.Error())
}
}
func TestDottedKeys(t *testing.T) {
tree, err := Load(`
name = "Orange"
physical.color = "orange"
physical.shape = "round"
site."google.com" = true`)
assertTree(t, tree, err, map[string]interface{}{
"name": "Orange",
"physical": map[string]interface{}{
"color": "orange",
"shape": "round",
},
"site": map[string]interface{}{
"google.com": true,
},
})
}
func TestInvalidDottedKeyEmptyGroup(t *testing.T) {
_, err := Load(`a..b = true`)
if err == nil {
t.Fatal("should return an error")
}
if err.Error() != "(1, 1): invalid key: expecting key part after dot" {
t.Fatalf("invalid error message: %s", err)
}
}
func TestAccidentalNewlines(t *testing.T) {
expected := "The quick brown fox jumps over the lazy dog."
tree, err := Load(`str1 = "The quick brown fox jumps over the lazy dog."
str2 = """
The quick brown \
fox jumps over \
the lazy dog."""
str3 = """\
The quick brown \` + " " + `
fox jumps over \` + " " + `
the lazy dog.\` + " " + `
"""`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := tree.Get("str1")
if got != expected {
t.Errorf("expected '%s', got '%s'", expected, got)
}
got = tree.Get("str2")
if got != expected {
t.Errorf("expected '%s', got '%s'", expected, got)
}
got = tree.Get("str3")
if got != expected {
t.Errorf("expected '%s', got '%s'", expected, got)
}
}
+201
View File
@@ -0,0 +1,201 @@
# Query package
## Overview
Package query performs JSONPath-like queries on a TOML document.
The query path implementation is based loosely on the JSONPath specification:
http://goessner.net/articles/JsonPath/.
The idea behind a query path is to allow quick access to any element, or set
of elements within TOML document, with a single expression.
```go
result, err := query.CompileAndExecute("$.foo.bar.baz", tree)
```
This is roughly equivalent to:
```go
next := tree.Get("foo")
if next != nil {
next = next.Get("bar")
if next != nil {
next = next.Get("baz")
}
}
result := next
```
err is nil if any parsing exception occurs.
If no node in the tree matches the query, result will simply contain an empty list of
items.
As illustrated above, the query path is much more efficient, especially since
the structure of the TOML file can vary. Rather than making assumptions about
a document's structure, a query allows the programmer to make structured
requests into the document, and get zero or more values as a result.
## Query syntax
The syntax of a query begins with a root token, followed by any number
sub-expressions:
```
$
Root of the TOML tree. This must always come first.
.name
Selects child of this node, where 'name' is a TOML key
name.
['name']
Selects child of this node, where 'name' is a string
containing a TOML key name.
[index]
Selcts child array element at 'index'.
..expr
Recursively selects all children, filtered by an a union,
index, or slice expression.
..*
Recursive selection of all nodes at this point in the
tree.
.*
Selects all children of the current node.
[expr,expr]
Union operator - a logical 'or' grouping of two or more
sub-expressions: index, key name, or filter.
[start:end:step]
Slice operator - selects array elements from start to
end-1, at the given step. All three arguments are
optional.
[?(filter)]
Named filter expression - the function 'filter' is
used to filter children at this node.
```
## Query Indexes And Slices
Index expressions perform no bounds checking, and will contribute no
values to the result set if the provided index or index range is invalid.
Negative indexes represent values from the end of the array, counting backwards.
```go
// select the last index of the array named 'foo'
query.CompileAndExecute("$.foo[-1]", tree)
```
Slice expressions are supported, by using ':' to separate a start/end index pair.
```go
// select up to the first five elements in the array
query.CompileAndExecute("$.foo[0:5]", tree)
```
Slice expressions also allow negative indexes for the start and stop
arguments.
```go
// select all array elements except the last one.
query.CompileAndExecute("$.foo[0:-1]", tree)
```
Slice expressions may have an optional stride/step parameter:
```go
// select every other element
query.CompileAndExecute("$.foo[0::2]", tree)
```
Slice start and end parameters are also optional:
```go
// these are all equivalent and select all the values in the array
query.CompileAndExecute("$.foo[:]", tree)
query.CompileAndExecute("$.foo[::]", tree)
query.CompileAndExecute("$.foo[::1]", tree)
query.CompileAndExecute("$.foo[0:]", tree)
query.CompileAndExecute("$.foo[0::]", tree)
query.CompileAndExecute("$.foo[0::1]", tree)
```
## Query Filters
Query filters are used within a Union [,] or single Filter [] expression.
A filter only allows nodes that qualify through to the next expression,
and/or into the result set.
```go
// returns children of foo that are permitted by the 'bar' filter.
query.CompileAndExecute("$.foo[?(bar)]", tree)
```
There are several filters provided with the library:
```
tree
Allows nodes of type Tree.
int
Allows nodes of type int64.
float
Allows nodes of type float64.
string
Allows nodes of type string.
time
Allows nodes of type time.Time.
bool
Allows nodes of type bool.
```
## Query Results
An executed query returns a Result object. This contains the nodes
in the TOML tree that qualify the query expression. Position information
is also available for each value in the set.
```go
// display the results of a query
results := query.CompileAndExecute("$.foo.bar.baz", tree)
for idx, value := results.Values() {
fmt.Println("%v: %v", results.Positions()[idx], value)
}
```
## Compiled Queries
Queries may be executed directly on a Tree object, or compiled ahead
of time and executed discretely. The former is more convenient, but has the
penalty of having to recompile the query expression each time.
```go
// basic query
results := query.CompileAndExecute("$.foo.bar.baz", tree)
// compiled query
query, err := toml.Compile("$.foo.bar.baz")
results := query.Execute(tree)
// run the compiled query again on a different tree
moreResults := query.Execute(anotherTree)
```
## User Defined Query Filters
Filter expressions may also be user defined by using the SetFilter()
function on the Query object. The function must return true/false, which
signifies if the passed node is kept or discarded, respectively.
```go
// create a query that references a user-defined filter
query, _ := query.Compile("$[?(bazOnly)]")
// define the filter, and assign it to the query
query.SetFilter("bazOnly", func(node interface{}) bool{
if tree, ok := node.(*Tree); ok {
return tree.Has("baz")
}
return false // reject all other node types
})
// run the query
query.Execute(tree)
```
+13 -15
View File
@@ -25,7 +25,7 @@
// items. // items.
// //
// As illustrated above, the query path is much more efficient, especially since // As illustrated above, the query path is much more efficient, especially since
// the structure of the TOML file can vary. Rather than making assumptions about // the structure of the TOML file can vary. Rather than making assumptions about
// a document's structure, a query allows the programmer to make structured // 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. // requests into the document, and get zero or more values as a result.
// //
@@ -35,7 +35,7 @@
// sub-expressions: // sub-expressions:
// //
// $ // $
// Root of the TOML tree. This must always come first. // Root of the TOML tree. This must always come first.
// .name // .name
// Selects child of this node, where 'name' is a TOML key // Selects child of this node, where 'name' is a TOML key
// name. // name.
@@ -57,7 +57,7 @@
// sub-expressions: index, key name, or filter. // sub-expressions: index, key name, or filter.
// [start:end:step] // [start:end:step]
// Slice operator - selects array elements from start to // Slice operator - selects array elements from start to
// end-1, at the given step. All three arguments are // end-1, at the given step. All three arguments are
// optional. // optional.
// [?(filter)] // [?(filter)]
// Named filter expression - the function 'filter' is // Named filter expression - the function 'filter' is
@@ -80,25 +80,23 @@
// Slice expressions also allow negative indexes for the start and stop // Slice expressions also allow negative indexes for the start and stop
// arguments. // arguments.
// //
// // select all array elements. // // select all array elements except the last one.
// query.CompileAndExecute("$.foo[0:-1]", tree) // query.CompileAndExecute("$.foo[0:-1]", tree)
// //
// Slice expressions may have an optional stride/step parameter: // Slice expressions may have an optional stride/step parameter:
// //
// // select every other element // // select every other element
// query.CompileAndExecute("$.foo[0:-1:2]", tree) // query.CompileAndExecute("$.foo[0::2]", tree)
// //
// Slice start and end parameters are also optional: // Slice start and end parameters are also optional:
// //
// // these are all equivalent and select all the values in the array // // these are all equivalent and select all the values in the array
// query.CompileAndExecute("$.foo[:]", tree) // query.CompileAndExecute("$.foo[:]", tree)
// query.CompileAndExecute("$.foo[0:]", tree) // query.CompileAndExecute("$.foo[::]", tree)
// query.CompileAndExecute("$.foo[:-1]", tree)
// query.CompileAndExecute("$.foo[0:-1:]", tree)
// query.CompileAndExecute("$.foo[::1]", tree) // query.CompileAndExecute("$.foo[::1]", tree)
// query.CompileAndExecute("$.foo[0:]", tree)
// query.CompileAndExecute("$.foo[0::]", tree)
// query.CompileAndExecute("$.foo[0::1]", tree) // query.CompileAndExecute("$.foo[0::1]", tree)
// query.CompileAndExecute("$.foo[:-1:1]", tree)
// query.CompileAndExecute("$.foo[0:-1:1]", tree)
// //
// Query Filters // Query Filters
// //
@@ -126,8 +124,8 @@
// //
// Query Results // Query Results
// //
// An executed query returns a Result object. This contains the nodes // An executed query returns a Result object. This contains the nodes
// in the TOML tree that qualify the query expression. Position information // in the TOML tree that qualify the query expression. Position information
// is also available for each value in the set. // is also available for each value in the set.
// //
// // display the results of a query // // display the results of a query
@@ -139,7 +137,7 @@
// Compiled Queries // Compiled Queries
// //
// Queries may be executed directly on a Tree object, or compiled ahead // Queries may be executed directly on a Tree object, or compiled ahead
// of time and executed discretely. The former is more convienent, but has the // of time and executed discretely. The former is more convenient, but has the
// penalty of having to recompile the query expression each time. // penalty of having to recompile the query expression each time.
// //
// // basic query // // basic query
@@ -155,7 +153,7 @@
// User Defined Query Filters // User Defined Query Filters
// //
// Filter expressions may also be user defined by using the SetFilter() // Filter expressions may also be user defined by using the SetFilter()
// function on the Query object. The function must return true/false, which // function on the Query object. The function must return true/false, which
// signifies if the passed node is kept or discarded, respectively. // signifies if the passed node is kept or discarded, respectively.
// //
// // create a query that references a user-defined filter // // create a query that references a user-defined filter
@@ -166,7 +164,7 @@
// if tree, ok := node.(*Tree); ok { // if tree, ok := node.(*Tree); ok {
// return tree.Has("baz") // return tree.Has("baz")
// } // }
// return false // reject all other node types // return false // reject all other node types
// }) // })
// //
// // run the query // // run the query
+4 -4
View File
@@ -7,10 +7,10 @@ package query
import ( import (
"fmt" "fmt"
"github.com/pelletier/go-toml"
"strconv" "strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/pelletier/go-toml"
) )
// Lexer state function // Lexer state function
@@ -55,7 +55,7 @@ func (l *queryLexer) nextStart() {
func (l *queryLexer) emit(t tokenType) { func (l *queryLexer) emit(t tokenType) {
l.tokens <- token{ l.tokens <- token{
Position: toml.Position{Line:l.line, Col:l.col}, Position: toml.Position{Line: l.line, Col: l.col},
typ: t, typ: t,
val: l.input[l.start:l.pos], val: l.input[l.start:l.pos],
} }
@@ -64,7 +64,7 @@ func (l *queryLexer) emit(t tokenType) {
func (l *queryLexer) emitWithValue(t tokenType, value string) { func (l *queryLexer) emitWithValue(t tokenType, value string) {
l.tokens <- token{ l.tokens <- token{
Position: toml.Position{Line:l.line, Col:l.col}, Position: toml.Position{Line: l.line, Col: l.col},
typ: t, typ: t,
val: value, val: value,
} }
@@ -92,7 +92,7 @@ func (l *queryLexer) backup() {
func (l *queryLexer) errorf(format string, args ...interface{}) queryLexStateFn { func (l *queryLexer) errorf(format string, args ...interface{}) queryLexStateFn {
l.tokens <- token{ l.tokens <- token{
Position: toml.Position{Line:l.line, Col:l.col}, Position: toml.Position{Line: l.line, Col: l.col},
typ: tokenError, typ: tokenError,
val: fmt.Sprintf(format, args...), val: fmt.Sprintf(format, args...),
} }
+1 -1
View File
@@ -1,8 +1,8 @@
package query package query
import ( import (
"testing"
"github.com/pelletier/go-toml" "github.com/pelletier/go-toml"
"testing"
) )
func testQLFlow(t *testing.T, input string, expectedFlow []token) { func testQLFlow(t *testing.T, input string, expectedFlow []token) {
+117 -38
View File
@@ -2,6 +2,8 @@ package query
import ( import (
"fmt" "fmt"
"reflect"
"github.com/pelletier/go-toml" "github.com/pelletier/go-toml"
) )
@@ -44,16 +46,16 @@ func newMatchKeyFn(name string) *matchKeyFn {
func (f *matchKeyFn) call(node interface{}, ctx *queryContext) { func (f *matchKeyFn) call(node interface{}, ctx *queryContext) {
if array, ok := node.([]*toml.Tree); ok { if array, ok := node.([]*toml.Tree); ok {
for _, tree := range array { for _, tree := range array {
item := tree.Get(f.Name) item := tree.GetPath([]string{f.Name})
if item != nil { if item != nil {
ctx.lastPosition = tree.GetPosition(f.Name) ctx.lastPosition = tree.GetPositionPath([]string{f.Name})
f.next.call(item, ctx) f.next.call(item, ctx)
} }
} }
} else if tree, ok := node.(*toml.Tree); ok { } else if tree, ok := node.(*toml.Tree); ok {
item := tree.Get(f.Name) item := tree.GetPath([]string{f.Name})
if item != nil { if item != nil {
ctx.lastPosition = tree.GetPosition(f.Name) ctx.lastPosition = tree.GetPositionPath([]string{f.Name})
f.next.call(item, ctx) f.next.call(item, ctx)
} }
} }
@@ -70,53 +72,130 @@ func newMatchIndexFn(idx int) *matchIndexFn {
} }
func (f *matchIndexFn) call(node interface{}, ctx *queryContext) { func (f *matchIndexFn) call(node interface{}, ctx *queryContext) {
if arr, ok := node.([]interface{}); ok { v := reflect.ValueOf(node)
if f.Idx < len(arr) && f.Idx >= 0 { if v.Kind() == reflect.Slice {
if treesArray, ok := node.([]*toml.Tree); ok { if v.Len() == 0 {
if len(treesArray) > 0 { return
ctx.lastPosition = treesArray[0].Position() }
}
} // Manage negative values
f.next.call(arr[f.Idx], ctx) idx := f.Idx
if idx < 0 {
idx += v.Len()
}
if 0 <= idx && idx < v.Len() {
callNextIndexSlice(f.next, node, ctx, v.Index(idx).Interface())
} }
} }
} }
func callNextIndexSlice(next pathFn, node interface{}, ctx *queryContext, value interface{}) {
if treesArray, ok := node.([]*toml.Tree); ok {
ctx.lastPosition = treesArray[0].Position()
}
next.call(value, ctx)
}
// filter by slicing // filter by slicing
type matchSliceFn struct { type matchSliceFn struct {
matchBase matchBase
Start, End, Step int Start, End, Step *int
} }
func newMatchSliceFn(start, end, step int) *matchSliceFn { func newMatchSliceFn() *matchSliceFn {
return &matchSliceFn{Start: start, End: end, Step: step} return &matchSliceFn{}
}
func (f *matchSliceFn) setStart(start int) *matchSliceFn {
f.Start = &start
return f
}
func (f *matchSliceFn) setEnd(end int) *matchSliceFn {
f.End = &end
return f
}
func (f *matchSliceFn) setStep(step int) *matchSliceFn {
f.Step = &step
return f
} }
func (f *matchSliceFn) call(node interface{}, ctx *queryContext) { func (f *matchSliceFn) call(node interface{}, ctx *queryContext) {
if arr, ok := node.([]interface{}); ok { v := reflect.ValueOf(node)
// adjust indexes for negative values, reverse ordering if v.Kind() == reflect.Slice {
realStart, realEnd := f.Start, f.End if v.Len() == 0 {
if realStart < 0 { return
realStart = len(arr) + realStart
} }
if realEnd < 0 {
realEnd = len(arr) + realEnd var start, end, step int
// Initialize step
if f.Step != nil {
step = *f.Step
} else {
step = 1
} }
if realEnd < realStart {
realEnd, realStart = realStart, realEnd // swap // Initialize start
} if f.Start != nil {
// loop and gather start = *f.Start
for idx := realStart; idx < realEnd; idx += f.Step { // Manage negative values
if treesArray, ok := node.([]*toml.Tree); ok { if start < 0 {
if len(treesArray) > 0 { start += v.Len()
ctx.lastPosition = treesArray[0].Position() }
} // Manage out of range values
start = max(start, 0)
start = min(start, v.Len()-1)
} else if step > 0 {
start = 0
} else {
start = v.Len() - 1
}
// Initialize end
if f.End != nil {
end = *f.End
// Manage negative values
if end < 0 {
end += v.Len()
}
// Manage out of range values
end = max(end, -1)
end = min(end, v.Len())
} else if step > 0 {
end = v.Len()
} else {
end = -1
}
// Loop on values
if step > 0 {
for idx := start; idx < end; idx += step {
callNextIndexSlice(f.next, node, ctx, v.Index(idx).Interface())
}
} else {
for idx := start; idx > end; idx += step {
callNextIndexSlice(f.next, node, ctx, v.Index(idx).Interface())
} }
f.next.call(arr[idx], ctx)
} }
} }
} }
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// match anything // match anything
type matchAnyFn struct { type matchAnyFn struct {
matchBase matchBase
@@ -129,8 +208,8 @@ func newMatchAnyFn() *matchAnyFn {
func (f *matchAnyFn) call(node interface{}, ctx *queryContext) { func (f *matchAnyFn) call(node interface{}, ctx *queryContext) {
if tree, ok := node.(*toml.Tree); ok { if tree, ok := node.(*toml.Tree); ok {
for _, k := range tree.Keys() { for _, k := range tree.Keys() {
v := tree.Get(k) v := tree.GetPath([]string{k})
ctx.lastPosition = tree.GetPosition(k) ctx.lastPosition = tree.GetPositionPath([]string{k})
f.next.call(v, ctx) f.next.call(v, ctx)
} }
} }
@@ -168,8 +247,8 @@ func (f *matchRecursiveFn) call(node interface{}, ctx *queryContext) {
var visit func(tree *toml.Tree) var visit func(tree *toml.Tree)
visit = func(tree *toml.Tree) { visit = func(tree *toml.Tree) {
for _, k := range tree.Keys() { for _, k := range tree.Keys() {
v := tree.Get(k) v := tree.GetPath([]string{k})
ctx.lastPosition = tree.GetPosition(k) ctx.lastPosition = tree.GetPositionPath([]string{k})
f.next.call(v, ctx) f.next.call(v, ctx)
switch node := v.(type) { switch node := v.(type) {
case *toml.Tree: case *toml.Tree:
@@ -207,9 +286,9 @@ func (f *matchFilterFn) call(node interface{}, ctx *queryContext) {
switch castNode := node.(type) { switch castNode := node.(type) {
case *toml.Tree: case *toml.Tree:
for _, k := range castNode.Keys() { for _, k := range castNode.Keys() {
v := castNode.Get(k) v := castNode.GetPath([]string{k})
if fn(v) { if fn(v) {
ctx.lastPosition = castNode.GetPosition(k) ctx.lastPosition = castNode.GetPositionPath([]string{k})
f.next.call(v, ctx) f.next.call(v, ctx)
} }
} }
+20 -9
View File
@@ -2,7 +2,9 @@ package query
import ( import (
"fmt" "fmt"
"strconv"
"testing" "testing"
"github.com/pelletier/go-toml" "github.com/pelletier/go-toml"
) )
@@ -19,8 +21,17 @@ func pathString(root pathFn) string {
result += fmt.Sprintf("{%d}", fn.Idx) result += fmt.Sprintf("{%d}", fn.Idx)
result += pathString(fn.next) result += pathString(fn.next)
case *matchSliceFn: case *matchSliceFn:
result += fmt.Sprintf("{%d:%d:%d}", startString, endString, stepString := "nil", "nil", "nil"
fn.Start, fn.End, fn.Step) if fn.Start != nil {
startString = strconv.Itoa(*fn.Start)
}
if fn.End != nil {
endString = strconv.Itoa(*fn.End)
}
if fn.Step != nil {
stepString = strconv.Itoa(*fn.Step)
}
result += fmt.Sprintf("{%s:%s:%s}", startString, endString, stepString)
result += pathString(fn.next) result += pathString(fn.next)
case *matchAnyFn: case *matchAnyFn:
result += "{}" result += "{}"
@@ -110,7 +121,7 @@ func TestPathSliceStart(t *testing.T) {
assertPath(t, assertPath(t,
"$[123:]", "$[123:]",
buildPath( buildPath(
newMatchSliceFn(123, maxInt, 1), newMatchSliceFn().setStart(123),
)) ))
} }
@@ -118,7 +129,7 @@ func TestPathSliceStartEnd(t *testing.T) {
assertPath(t, assertPath(t,
"$[123:456]", "$[123:456]",
buildPath( buildPath(
newMatchSliceFn(123, 456, 1), newMatchSliceFn().setStart(123).setEnd(456),
)) ))
} }
@@ -126,7 +137,7 @@ func TestPathSliceStartEndColon(t *testing.T) {
assertPath(t, assertPath(t,
"$[123:456:]", "$[123:456:]",
buildPath( buildPath(
newMatchSliceFn(123, 456, 1), newMatchSliceFn().setStart(123).setEnd(456),
)) ))
} }
@@ -134,7 +145,7 @@ func TestPathSliceStartStep(t *testing.T) {
assertPath(t, assertPath(t,
"$[123::7]", "$[123::7]",
buildPath( buildPath(
newMatchSliceFn(123, maxInt, 7), newMatchSliceFn().setStart(123).setStep(7),
)) ))
} }
@@ -142,7 +153,7 @@ func TestPathSliceEndStep(t *testing.T) {
assertPath(t, assertPath(t,
"$[:456:7]", "$[:456:7]",
buildPath( buildPath(
newMatchSliceFn(0, 456, 7), newMatchSliceFn().setEnd(456).setStep(7),
)) ))
} }
@@ -150,7 +161,7 @@ func TestPathSliceStep(t *testing.T) {
assertPath(t, assertPath(t,
"$[::7]", "$[::7]",
buildPath( buildPath(
newMatchSliceFn(0, maxInt, 7), newMatchSliceFn().setStep(7),
)) ))
} }
@@ -158,7 +169,7 @@ func TestPathSliceAll(t *testing.T) {
assertPath(t, assertPath(t,
"$[123:456:7]", "$[123:456:7]",
buildPath( buildPath(
newMatchSliceFn(123, 456, 7), newMatchSliceFn().setStart(123).setEnd(456).setStep(7),
)) ))
} }
+12 -9
View File
@@ -203,12 +203,13 @@ loop: // labeled loop for easy breaking
func (p *queryParser) parseSliceExpr() queryParserStateFn { func (p *queryParser) parseSliceExpr() queryParserStateFn {
// init slice to grab all elements // init slice to grab all elements
start, end, step := 0, maxInt, 1 var start, end, step *int = nil, nil, nil
// parse optional start // parse optional start
tok := p.getToken() tok := p.getToken()
if tok.typ == tokenInteger { if tok.typ == tokenInteger {
start = tok.Int() v := tok.Int()
start = &v
tok = p.getToken() tok = p.getToken()
} }
if tok.typ != tokenColon { if tok.typ != tokenColon {
@@ -218,11 +219,12 @@ func (p *queryParser) parseSliceExpr() queryParserStateFn {
// parse optional end // parse optional end
tok = p.getToken() tok = p.getToken()
if tok.typ == tokenInteger { if tok.typ == tokenInteger {
end = tok.Int() v := tok.Int()
end = &v
tok = p.getToken() tok = p.getToken()
} }
if tok.typ == tokenRightBracket { if tok.typ == tokenRightBracket {
p.query.appendPath(newMatchSliceFn(start, end, step)) p.query.appendPath(&matchSliceFn{Start: start, End: end, Step: step})
return p.parseMatchExpr return p.parseMatchExpr
} }
if tok.typ != tokenColon { if tok.typ != tokenColon {
@@ -232,17 +234,18 @@ func (p *queryParser) parseSliceExpr() queryParserStateFn {
// parse optional step // parse optional step
tok = p.getToken() tok = p.getToken()
if tok.typ == tokenInteger { if tok.typ == tokenInteger {
step = tok.Int() v := tok.Int()
if step < 0 { if v == 0 {
return p.parseError(tok, "step must be a positive value") return p.parseError(tok, "step cannot be zero")
} }
step = &v
tok = p.getToken() tok = p.getToken()
} }
if tok.typ != tokenRightBracket { if tok.typ != tokenRightBracket {
return p.parseError(tok, "expected ']'") return p.parseError(tok, "expected ']'")
} }
p.query.appendPath(newMatchSliceFn(start, end, step)) p.query.appendPath(&matchSliceFn{Start: start, End: end, Step: step})
return p.parseMatchExpr return p.parseMatchExpr
} }
@@ -253,7 +256,7 @@ func (p *queryParser) parseFilterExpr() queryParserStateFn {
} }
tok = p.getToken() tok = p.getToken()
if tok.typ != tokenKey && tok.typ != tokenString { if tok.typ != tokenKey && tok.typ != tokenString {
return p.parseError(tok, "expected key or string for filter funciton name") return p.parseError(tok, "expected key or string for filter function name")
} }
name := tok.val name := tok.val
tok = p.getToken() tok = p.getToken()
+226 -95
View File
@@ -7,6 +7,7 @@ import (
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/pelletier/go-toml" "github.com/pelletier/go-toml"
) )
@@ -77,6 +78,19 @@ func assertValue(t *testing.T, result, ref interface{}) {
} }
} }
func assertParseError(t *testing.T, query string, errString string) {
_, err := Compile(query)
if err == nil {
t.Error("error should be non-nil")
return
}
if err.Error() != errString {
t.Errorf("error does not match")
t.Log("test:", err.Error())
t.Log("ref: ", errString)
}
}
func assertQueryPositions(t *testing.T, tomlDoc string, query string, ref []interface{}) { func assertQueryPositions(t *testing.T, tomlDoc string, query string, ref []interface{}) {
tree, err := toml.Load(tomlDoc) tree, err := toml.Load(tomlDoc)
if err != nil { if err != nil {
@@ -127,54 +141,213 @@ func TestQueryKeyString(t *testing.T) {
}) })
} }
func TestQueryIndex(t *testing.T) { func TestQueryKeyUnicodeString(t *testing.T) {
assertQueryPositions(t, assertQueryPositions(t,
"[foo]\na = [1,2,3,4,5,6,7,8,9,0]", "['f𝟘.o']\na = 42",
"$.foo.a[5]", "$['f𝟘.o']['a']",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
int64(6), toml.Position{2, 1}, int64(42), toml.Position{2, 1},
}, },
}) })
} }
func TestQueryIndexError1(t *testing.T) {
assertParseError(t, "$.foo.a[5", "(1, 10): expected ',' or ']', not ''")
}
func TestQueryIndexError2(t *testing.T) {
assertParseError(t, "$.foo.a[]", "(1, 9): expected union sub expression, not ']', 0")
}
func TestQueryIndex(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[5]",
[]interface{}{
queryTestNode{int64(5), toml.Position{2, 1}},
})
}
func TestQueryIndexNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[-2]",
[]interface{}{
queryTestNode{int64(8), toml.Position{2, 1}},
})
}
func TestQueryIndexWrong(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[99]",
[]interface{}{})
}
func TestQueryIndexEmpty(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = []",
"$.foo.a[5]",
[]interface{}{})
}
func TestQueryIndexTree(t *testing.T) {
assertQueryPositions(t,
"[[foo]]\na = [0,1,2,3,4,5,6,7,8,9]\n[[foo]]\nb = 3",
"$.foo[1].b",
[]interface{}{
queryTestNode{int64(3), toml.Position{4, 1}},
})
}
func TestQuerySliceError1(t *testing.T) {
assertParseError(t, "$.foo.a[3:?]", "(1, 11): expected ']' or ':'")
}
func TestQuerySliceError2(t *testing.T) {
assertParseError(t, "$.foo.a[:::]", "(1, 11): expected ']'")
}
func TestQuerySliceError3(t *testing.T) {
assertParseError(t, "$.foo.a[::0]", "(1, 11): step cannot be zero")
}
func TestQuerySliceRange(t *testing.T) { func TestQuerySliceRange(t *testing.T) {
assertQueryPositions(t, assertQueryPositions(t,
"[foo]\na = [1,2,3,4,5,6,7,8,9,0]", "[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[0:5]", "$.foo.a[:5]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{int64(0), toml.Position{2, 1}},
int64(1), toml.Position{2, 1}, queryTestNode{int64(1), toml.Position{2, 1}},
}, queryTestNode{int64(2), toml.Position{2, 1}},
queryTestNode{ queryTestNode{int64(3), toml.Position{2, 1}},
int64(2), toml.Position{2, 1}, queryTestNode{int64(4), 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) { func TestQuerySliceStep(t *testing.T) {
assertQueryPositions(t, assertQueryPositions(t,
"[foo]\na = [1,2,3,4,5,6,7,8,9,0]", "[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[0:5:2]", "$.foo.a[0:5:2]",
[]interface{}{
queryTestNode{int64(0), toml.Position{2, 1}},
queryTestNode{int64(2), toml.Position{2, 1}},
queryTestNode{int64(4), toml.Position{2, 1}},
})
}
func TestQuerySliceStartNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[-3:]",
[]interface{}{
queryTestNode{int64(7), toml.Position{2, 1}},
queryTestNode{int64(8), toml.Position{2, 1}},
queryTestNode{int64(9), toml.Position{2, 1}},
})
}
func TestQuerySliceEndNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[:-6]",
[]interface{}{
queryTestNode{int64(0), toml.Position{2, 1}},
queryTestNode{int64(1), toml.Position{2, 1}},
queryTestNode{int64(2), toml.Position{2, 1}},
queryTestNode{int64(3), toml.Position{2, 1}},
})
}
func TestQuerySliceStepNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[::-2]",
[]interface{}{
queryTestNode{int64(9), toml.Position{2, 1}},
queryTestNode{int64(7), toml.Position{2, 1}},
queryTestNode{int64(5), toml.Position{2, 1}},
queryTestNode{int64(3), toml.Position{2, 1}},
queryTestNode{int64(1), toml.Position{2, 1}},
})
}
func TestQuerySliceStartOverRange(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[-99:3]",
[]interface{}{
queryTestNode{int64(0), toml.Position{2, 1}},
queryTestNode{int64(1), toml.Position{2, 1}},
queryTestNode{int64(2), toml.Position{2, 1}},
})
}
func TestQuerySliceStartOverRangeNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[99:7:-1]",
[]interface{}{
queryTestNode{int64(9), toml.Position{2, 1}},
queryTestNode{int64(8), toml.Position{2, 1}},
})
}
func TestQuerySliceEndOverRange(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[7:99]",
[]interface{}{
queryTestNode{int64(7), toml.Position{2, 1}},
queryTestNode{int64(8), toml.Position{2, 1}},
queryTestNode{int64(9), toml.Position{2, 1}},
})
}
func TestQuerySliceEndOverRangeNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[2:-99:-1]",
[]interface{}{
queryTestNode{int64(2), toml.Position{2, 1}},
queryTestNode{int64(1), toml.Position{2, 1}},
queryTestNode{int64(0), toml.Position{2, 1}},
})
}
func TestQuerySliceWrongRange(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[5:3]",
[]interface{}{})
}
func TestQuerySliceWrongRangeNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[3:5:-1]",
[]interface{}{})
}
func TestQuerySliceEmpty(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = []",
"$.foo.a[5:]",
[]interface{}{})
}
func TestQuerySliceTree(t *testing.T) {
assertQueryPositions(t,
"[[foo]]\na='nok'\n[[foo]]\na = [0,1,2,3,4,5,6,7,8,9]\n[[foo]]\na='ok'\nb = 3",
"$.foo[1:].a",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
int64(1), toml.Position{2, 1}, []interface{}{
}, int64(0), int64(1), int64(2), int64(3), int64(4),
queryTestNode{ int64(5), int64(6), int64(7), int64(8), int64(9)},
int64(3), toml.Position{2, 1}, toml.Position{4, 1}},
}, queryTestNode{"ok", toml.Position{6, 1}},
queryTestNode{
int64(5), toml.Position{2, 1},
},
}) })
} }
@@ -264,12 +437,8 @@ func TestQueryRecursionAll(t *testing.T) {
"b": int64(2), "b": int64(2),
}, toml.Position{1, 1}, }, toml.Position{1, 1},
}, },
queryTestNode{ queryTestNode{int64(1), toml.Position{2, 1}},
int64(1), toml.Position{2, 1}, queryTestNode{int64(2), toml.Position{3, 1}},
},
queryTestNode{
int64(2), toml.Position{3, 1},
},
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"foo": map[string]interface{}{ "foo": map[string]interface{}{
@@ -284,12 +453,8 @@ func TestQueryRecursionAll(t *testing.T) {
"b": int64(4), "b": int64(4),
}, toml.Position{4, 1}, }, toml.Position{4, 1},
}, },
queryTestNode{ queryTestNode{int64(3), toml.Position{5, 1}},
int64(3), toml.Position{5, 1}, queryTestNode{int64(4), toml.Position{6, 1}},
},
queryTestNode{
int64(4), toml.Position{6, 1},
},
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"foo": map[string]interface{}{ "foo": map[string]interface{}{
@@ -304,12 +469,8 @@ func TestQueryRecursionAll(t *testing.T) {
"b": int64(6), "b": int64(6),
}, toml.Position{7, 1}, }, toml.Position{7, 1},
}, },
queryTestNode{ queryTestNode{int64(5), toml.Position{8, 1}},
int64(5), toml.Position{8, 1}, queryTestNode{int64(6), toml.Position{9, 1}},
},
queryTestNode{
int64(6), toml.Position{9, 1},
},
}) })
} }
@@ -357,57 +518,30 @@ func TestQueryFilterFn(t *testing.T) {
assertQueryPositions(t, string(buff), assertQueryPositions(t, string(buff),
"$..[?(int)]", "$..[?(int)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{int64(8001), toml.Position{13, 1}},
int64(8001), toml.Position{13, 1}, queryTestNode{int64(8001), toml.Position{13, 1}},
}, queryTestNode{int64(8002), toml.Position{13, 1}},
queryTestNode{ queryTestNode{int64(5000), toml.Position{14, 1}},
int64(8001), toml.Position{13, 1},
},
queryTestNode{
int64(8002), toml.Position{13, 1},
},
queryTestNode{
int64(5000), toml.Position{14, 1},
},
}) })
assertQueryPositions(t, string(buff), assertQueryPositions(t, string(buff),
"$..[?(string)]", "$..[?(string)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{"TOML Example", toml.Position{3, 1}},
"TOML Example", toml.Position{3, 1}, queryTestNode{"Tom Preston-Werner", toml.Position{6, 1}},
}, queryTestNode{"GitHub", toml.Position{7, 1}},
queryTestNode{ queryTestNode{"GitHub Cofounder & CEO\nLikes tater tots and beer.", toml.Position{8, 1}},
"Tom Preston-Werner", toml.Position{6, 1}, queryTestNode{"192.168.1.1", toml.Position{12, 1}},
}, queryTestNode{"10.0.0.1", toml.Position{21, 3}},
queryTestNode{ queryTestNode{"eqdc10", toml.Position{22, 3}},
"GitHub", toml.Position{7, 1}, queryTestNode{"10.0.0.2", toml.Position{25, 3}},
}, queryTestNode{"eqdc10", toml.Position{26, 3}},
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), assertQueryPositions(t, string(buff),
"$..[?(float)]", "$..[?(float)]",
[]interface{}{ []interface{}{
// no float values in document queryTestNode{4e-08, toml.Position{30, 1}},
}) })
tv, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z") tv, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
@@ -460,6 +594,7 @@ func TestQueryFilterFn(t *testing.T) {
[]interface{}{"gamma", "delta"}, []interface{}{"gamma", "delta"},
[]interface{}{int64(1), int64(2)}, []interface{}{int64(1), int64(2)},
}, },
"score": 4e-08,
}, toml.Position{28, 1}, }, toml.Position{28, 1},
}, },
}) })
@@ -467,16 +602,12 @@ func TestQueryFilterFn(t *testing.T) {
assertQueryPositions(t, string(buff), assertQueryPositions(t, string(buff),
"$..[?(time)]", "$..[?(time)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{tv, toml.Position{9, 1}},
tv, toml.Position{9, 1},
},
}) })
assertQueryPositions(t, string(buff), assertQueryPositions(t, string(buff),
"$..[?(bool)]", "$..[?(bool)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{true, toml.Position{15, 1}},
true, toml.Position{15, 1},
},
}) })
} }
+25 -31
View File
@@ -7,25 +7,26 @@ import (
"github.com/pelletier/go-toml" "github.com/pelletier/go-toml"
) )
func assertArrayContainsInAnyOrder(t *testing.T, array []interface{}, objects ...interface{}) { func assertArrayContainsInOrder(t *testing.T, array []interface{}, objects ...interface{}) {
if len(array) != len(objects) { if len(array) != len(objects) {
t.Fatalf("array contains %d objects but %d are expected", len(array), len(objects)) t.Fatalf("array contains %d objects but %d are expected", len(array), len(objects))
} }
for _, o := range objects { for i := 0; i < len(array); i++ {
found := false if array[i] != objects[i] {
for _, a := range array { t.Fatalf("wanted '%s', have '%s'", objects[i], array[i])
if a == o {
found = true
break
}
}
if !found {
t.Fatal(o, "not found in array", array)
} }
} }
} }
func checkQuery(t *testing.T, tree *toml.Tree, query string, objects ...interface{}) {
results, err := CompileAndExecute(query, tree)
if err != nil {
t.Fatal("unexpected error:", err)
}
assertArrayContainsInOrder(t, results.Values(), objects...)
}
func TestQueryExample(t *testing.T) { func TestQueryExample(t *testing.T) {
config, _ := toml.Load(` config, _ := toml.Load(`
[[book]] [[book]]
@@ -37,16 +38,18 @@ func TestQueryExample(t *testing.T) {
[[book]] [[book]]
title = "Neuromancer" title = "Neuromancer"
author = "William Gibson" author = "William Gibson"
`) `)
authors, err := CompileAndExecute("$.book.author", config)
if err != nil { checkQuery(t, config, "$.book.author", "Stephen King", "Ernest Hemmingway", "William Gibson")
t.Fatal("unexpected error:", err)
} checkQuery(t, config, "$.book[0].author", "Stephen King")
names := authors.Values() checkQuery(t, config, "$.book[-1].author", "William Gibson")
if len(names) != 3 { checkQuery(t, config, "$.book[1:].author", "Ernest Hemmingway", "William Gibson")
t.Fatalf("query should return 3 names but returned %d", len(names)) checkQuery(t, config, "$.book[-1:].author", "William Gibson")
} checkQuery(t, config, "$.book[::2].author", "Stephen King", "William Gibson")
assertArrayContainsInAnyOrder(t, names, "Stephen King", "Ernest Hemmingway", "William Gibson") checkQuery(t, config, "$.book[::-1].author", "William Gibson", "Ernest Hemmingway", "Stephen King")
checkQuery(t, config, "$.book[:].author", "Stephen King", "Ernest Hemmingway", "William Gibson")
checkQuery(t, config, "$.book[::].author", "Stephen King", "Ernest Hemmingway", "William Gibson")
} }
func TestQueryReadmeExample(t *testing.T) { func TestQueryReadmeExample(t *testing.T) {
@@ -56,16 +59,7 @@ user = "pelletier"
password = "mypassword" password = "mypassword"
`) `)
query, err := Compile("$..[user,password]") checkQuery(t, config, "$..[user,password]", "pelletier", "mypassword")
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) { func TestQueryPathNotPresent(t *testing.T) {
+5 -6
View File
@@ -1,9 +1,9 @@
package query package query
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"unicode"
"github.com/pelletier/go-toml" "github.com/pelletier/go-toml"
) )
@@ -92,11 +92,11 @@ func isSpace(r rune) bool {
} }
func isAlphanumeric(r rune) bool { func isAlphanumeric(r rune) bool {
return unicode.IsLetter(r) || r == '_' return 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || r == '_'
} }
func isDigit(r rune) bool { func isDigit(r rune) bool {
return unicode.IsNumber(r) return '0' <= r && r <= '9'
} }
func isHexDigit(r rune) bool { func isHexDigit(r rune) bool {
@@ -104,4 +104,3 @@ func isHexDigit(r rune) bool {
(r >= 'a' && r <= 'f') || (r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F') (r >= 'A' && r <= 'F')
} }
-88
View File
@@ -1,88 +0,0 @@
#!/bin/bash
# fail out of the script if anything here fails
set -e
# set the path to the present working directory
export GOPATH=`pwd`
function git_clone() {
path=$1
branch=$2
version=$3
if [ ! -d "src/$path" ]; then
mkdir -p src/$path
git clone https://$path.git src/$path
fi
pushd src/$path
git checkout "$branch"
git reset --hard "$version"
popd
}
# Remove potential previous runs
rm -rf src test_program_bin toml-test
# Run go vet
go vet ./...
go get github.com/pelletier/go-buffruneio
go get github.com/davecgh/go-spew/spew
# get code for BurntSushi TOML validation
# pinning all to 'HEAD' for version 0.3.x work (TODO: pin to commit hash when tests stabilize)
git_clone github.com/BurntSushi/toml master HEAD
git_clone github.com/BurntSushi/toml-test master HEAD #was: 0.2.0 HEAD
# build the BurntSushi test application
go build -o toml-test github.com/BurntSushi/toml-test
# vendorize the current lib for testing
# NOTE: this basically mocks an install without having to go back out to github for code
mkdir -p src/github.com/pelletier/go-toml/cmd
mkdir -p src/github.com/pelletier/go-toml/query
cp *.go *.toml src/github.com/pelletier/go-toml
cp -R cmd/* src/github.com/pelletier/go-toml/cmd
cp -R query/* src/github.com/pelletier/go-toml/query
go build -o test_program_bin src/github.com/pelletier/go-toml/cmd/test_program.go
# Run basic unit tests
go test github.com/pelletier/go-toml -covermode=count -coverprofile=coverage.out
go test github.com/pelletier/go-toml/cmd/tomljson
go test github.com/pelletier/go-toml/query
# run the entire BurntSushi test suite
if [[ $# -eq 0 ]] ; then
echo "Running all BurntSushi tests"
./toml-test ./test_program_bin | tee test_out
else
# run a specific test
test=$1
test_path='src/github.com/BurntSushi/toml-test/tests'
valid_test="$test_path/valid/$test"
invalid_test="$test_path/invalid/$test"
if [ -e "$valid_test.toml" ]; then
echo "Valid Test TOML for $test:"
echo "===="
cat "$valid_test.toml"
echo "Valid Test JSON for $test:"
echo "===="
cat "$valid_test.json"
echo "Go-TOML Output for $test:"
echo "===="
cat "$valid_test.toml" | ./test_program_bin
fi
if [ -e "$invalid_test.toml" ]; then
echo "Invalid Test TOML for $test:"
echo "===="
cat "$invalid_test.toml"
echo "Go-TOML Output for $test:"
echo "===="
echo "go-toml Output:"
cat "$invalid_test.toml" | ./test_program_bin
fi
fi
+13 -17
View File
@@ -1,10 +1,6 @@
package toml package toml
import ( import "fmt"
"fmt"
"strconv"
"unicode"
)
// Define tokens // Define tokens
type tokenType int type tokenType int
@@ -23,6 +19,8 @@ const (
tokenTrue tokenTrue
tokenFalse tokenFalse
tokenFloat tokenFloat
tokenInf
tokenNan
tokenEqual tokenEqual
tokenLeftBracket tokenLeftBracket
tokenRightBracket tokenRightBracket
@@ -32,7 +30,9 @@ const (
tokenRightParen tokenRightParen
tokenDoubleLeftBracket tokenDoubleLeftBracket
tokenDoubleRightBracket tokenDoubleRightBracket
tokenDate tokenLocalDate
tokenLocalTime
tokenTimeOffset
tokenKeyGroup tokenKeyGroup
tokenKeyGroupArray tokenKeyGroupArray
tokenComma tokenComma
@@ -55,6 +55,8 @@ var tokenTypeNames = []string{
"True", "True",
"False", "False",
"Float", "Float",
"Inf",
"NaN",
"=", "=",
"[", "[",
"]", "]",
@@ -64,7 +66,9 @@ var tokenTypeNames = []string{
")", ")",
"]]", "]]",
"[[", "[[",
"Date", "LocalDate",
"LocalTime",
"TimeOffset",
"KeyGroup", "KeyGroup",
"KeyGroupArray", "KeyGroupArray",
",", ",",
@@ -91,14 +95,6 @@ func (tt tokenType) String() string {
return "Unknown" 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 { func (t token) String() string {
switch t.typ { switch t.typ {
case tokenEOF: case tokenEOF:
@@ -115,7 +111,7 @@ func isSpace(r rune) bool {
} }
func isAlphanumeric(r rune) bool { func isAlphanumeric(r rune) bool {
return unicode.IsLetter(r) || r == '_' return 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || r == '_'
} }
func isKeyChar(r rune) bool { func isKeyChar(r rune) bool {
@@ -130,7 +126,7 @@ func isKeyStartChar(r rune) bool {
} }
func isDigit(r rune) bool { func isDigit(r rune) bool {
return unicode.IsNumber(r) return '0' <= r && r <= '9'
} }
func isHexDigit(r rune) bool { func isHexDigit(r rune) bool {
+3 -1
View File
@@ -25,7 +25,9 @@ func TestTokenStringer(t *testing.T) {
{tokenRightParen, ")"}, {tokenRightParen, ")"},
{tokenDoubleLeftBracket, "]]"}, {tokenDoubleLeftBracket, "]]"},
{tokenDoubleRightBracket, "[["}, {tokenDoubleRightBracket, "[["},
{tokenDate, "Date"}, {tokenLocalDate, "LocalDate"},
{tokenLocalTime, "LocalTime"},
{tokenTimeOffset, "TimeOffset"},
{tokenKeyGroup, "KeyGroup"}, {tokenKeyGroup, "KeyGroup"},
{tokenKeyGroupArray, "KeyGroupArray"}, {tokenKeyGroupArray, "KeyGroupArray"},
{tokenComma, ","}, {tokenComma, ","},
+284 -32
View File
@@ -4,26 +4,38 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"runtime" "runtime"
"strings" "strings"
) )
type tomlValue struct { type tomlValue struct {
value interface{} // string, int64, uint64, float64, bool, time.Time, [] of any of this list value interface{} // string, int64, uint64, float64, bool, time.Time, [] of any of this list
position Position comment string
commented bool
multiline bool
literal bool
position Position
} }
// Tree is the result of the parsing of a TOML file. // Tree is the result of the parsing of a TOML file.
type Tree struct { type Tree struct {
values map[string]interface{} // string -> *tomlValue, *Tree, []*Tree values map[string]interface{} // string -> *tomlValue, *Tree, []*Tree
position Position comment string
commented bool
inline bool
position Position
} }
func newTree() *Tree { func newTree() *Tree {
return newTreeWithPosition(Position{})
}
func newTreeWithPosition(pos Position) *Tree {
return &Tree{ return &Tree{
values: make(map[string]interface{}), values: make(map[string]interface{}),
position: Position{}, position: pos,
} }
} }
@@ -66,18 +78,15 @@ func (t *Tree) Keys() []string {
} }
// Get the value at key in the Tree. // Get the value at key in the Tree.
// Key is a dot-separated path (e.g. a.b.c). // 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. // Returns nil if the path does not exist in the tree.
// If keys is of length zero, the current tree is returned. // If keys is of length zero, the current tree is returned.
func (t *Tree) Get(key string) interface{} { func (t *Tree) Get(key string) interface{} {
if key == "" { if key == "" {
return t return t
} }
comps, err := parseKey(key) return t.GetPath(strings.Split(key, "."))
if err != nil {
return nil
}
return t.GetPath(comps)
} }
// GetPath returns the element in the tree indicated by 'keys'. // GetPath returns the element in the tree indicated by 'keys'.
@@ -114,6 +123,89 @@ func (t *Tree) GetPath(keys []string) interface{} {
} }
} }
// GetArray returns the value at key in the Tree.
// It returns []string, []int64, etc type if key has homogeneous lists
// Key is a dot-separated path (e.g. a.b.c) without single/double quoted strings.
// Returns nil if the path does not exist in the tree.
// If keys is of length zero, the current tree is returned.
func (t *Tree) GetArray(key string) interface{} {
if key == "" {
return t
}
return t.GetArrayPath(strings.Split(key, "."))
}
// GetArrayPath returns the element in the tree indicated by 'keys'.
// If keys is of length zero, the current tree is returned.
func (t *Tree) GetArrayPath(keys []string) interface{} {
if len(keys) == 0 {
return t
}
subtree := t
for _, intermediateKey := range keys[:len(keys)-1] {
value, exists := subtree.values[intermediateKey]
if !exists {
return nil
}
switch node := value.(type) {
case *Tree:
subtree = node
case []*Tree:
// go to most recent element
if len(node) == 0 {
return nil
}
subtree = node[len(node)-1]
default:
return nil // cannot navigate through other node types
}
}
// branch based on final node type
switch node := subtree.values[keys[len(keys)-1]].(type) {
case *tomlValue:
switch n := node.value.(type) {
case []interface{}:
return getArray(n)
default:
return node.value
}
default:
return node
}
}
// if homogeneous array, then return slice type object over []interface{}
func getArray(n []interface{}) interface{} {
var s []string
var i64 []int64
var f64 []float64
var bl []bool
for _, value := range n {
switch v := value.(type) {
case string:
s = append(s, v)
case int64:
i64 = append(i64, v)
case float64:
f64 = append(f64, v)
case bool:
bl = append(bl, v)
default:
return n
}
}
if len(s) == len(n) {
return s
} else if len(i64) == len(n) {
return i64
} else if len(f64) == len(n) {
return f64
} else if len(bl) == len(n) {
return bl
}
return n
}
// GetPosition returns the position of the given key. // GetPosition returns the position of the given key.
func (t *Tree) GetPosition(key string) Position { func (t *Tree) GetPosition(key string) Position {
if key == "" { if key == "" {
@@ -122,6 +214,50 @@ func (t *Tree) GetPosition(key string) Position {
return t.GetPositionPath(strings.Split(key, ".")) return t.GetPositionPath(strings.Split(key, "."))
} }
// SetPositionPath sets the position of element in the tree indicated by 'keys'.
// If keys is of length zero, the current tree position is set.
func (t *Tree) SetPositionPath(keys []string, pos Position) {
if len(keys) == 0 {
t.position = pos
return
}
subtree := t
for _, intermediateKey := range keys[:len(keys)-1] {
value, exists := subtree.values[intermediateKey]
if !exists {
return
}
switch node := value.(type) {
case *Tree:
subtree = node
case []*Tree:
// go to most recent element
if len(node) == 0 {
return
}
subtree = node[len(node)-1]
default:
return
}
}
// branch based on final node type
switch node := subtree.values[keys[len(keys)-1]].(type) {
case *tomlValue:
node.position = pos
return
case *Tree:
node.position = pos
return
case []*Tree:
// go to most recent element
if len(node) == 0 {
return
}
node[len(node)-1].position = pos
return
}
}
// GetPositionPath returns the element in the tree indicated by 'keys'. // GetPositionPath returns the element in the tree indicated by 'keys'.
// If keys is of length zero, the current tree is returned. // If keys is of length zero, the current tree is returned.
func (t *Tree) GetPositionPath(keys []string) Position { func (t *Tree) GetPositionPath(keys []string) Position {
@@ -173,22 +309,29 @@ func (t *Tree) GetDefault(key string, def interface{}) interface{} {
return val return val
} }
// Set an element in the tree. // SetOptions arguments are supplied to the SetWithOptions and SetPathWithOptions functions to modify marshalling behaviour.
// Key is a dot-separated path (e.g. a.b.c). // The default values within the struct are valid default options.
// Creates all necessary intermediate trees, if needed. type SetOptions struct {
func (t *Tree) Set(key string, value interface{}) { Comment string
t.SetPath(strings.Split(key, "."), value) Commented bool
Multiline bool
Literal bool
} }
// SetPath sets an element in the tree. // SetWithOptions is the same as Set, but allows you to provide formatting
// Keys is an array of path elements (e.g. {"a","b","c"}). // instructions to the key, that will be used by Marshal().
// Creates all necessary intermediate trees, if needed. func (t *Tree) SetWithOptions(key string, opts SetOptions, value interface{}) {
func (t *Tree) SetPath(keys []string, 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 subtree := t
for _, intermediateKey := range keys[:len(keys)-1] { for i, intermediateKey := range keys[:len(keys)-1] {
nextTree, exists := subtree.values[intermediateKey] nextTree, exists := subtree.values[intermediateKey]
if !exists { if !exists {
nextTree = newTree() nextTree = newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col})
subtree.values[intermediateKey] = nextTree // add new element here subtree.values[intermediateKey] = nextTree // add new element here
} }
switch node := nextTree.(type) { switch node := nextTree.(type) {
@@ -198,7 +341,8 @@ func (t *Tree) SetPath(keys []string, value interface{}) {
// go to most recent element // go to most recent element
if len(node) == 0 { if len(node) == 0 {
// create element if it does not exist // create element if it does not exist
subtree.values[intermediateKey] = append(node, newTree()) node = append(node, newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col}))
subtree.values[intermediateKey] = node
} }
subtree = node[len(node)-1] subtree = node[len(node)-1]
} }
@@ -206,20 +350,88 @@ func (t *Tree) SetPath(keys []string, value interface{}) {
var toInsert interface{} var toInsert interface{}
switch value.(type) { switch v := value.(type) {
case *Tree: case *Tree:
v.comment = opts.Comment
v.commented = opts.Commented
toInsert = value toInsert = value
case []*Tree: case []*Tree:
for i := range v {
v[i].commented = opts.Commented
}
toInsert = value toInsert = value
case *tomlValue: case *tomlValue:
toInsert = value v.comment = opts.Comment
v.commented = opts.Commented
v.multiline = opts.Multiline
v.literal = opts.Literal
toInsert = v
default: default:
toInsert = &tomlValue{value: value} toInsert = &tomlValue{value: value,
comment: opts.Comment,
commented: opts.Commented,
multiline: opts.Multiline,
literal: opts.Literal,
position: Position{Line: subtree.position.Line + len(subtree.values) + 1, Col: subtree.position.Col}}
} }
subtree.values[keys[len(keys)-1]] = toInsert 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 // createSubTree takes a tree and a key and create the necessary intermediate
// subtrees to create a subtree at that point. In-place. // subtrees to create a subtree at that point. In-place.
// //
@@ -229,11 +441,12 @@ func (t *Tree) SetPath(keys []string, value interface{}) {
// Returns nil on success, error object on failure // Returns nil on success, error object on failure
func (t *Tree) createSubTree(keys []string, pos Position) error { func (t *Tree) createSubTree(keys []string, pos Position) error {
subtree := t subtree := t
for _, intermediateKey := range keys { for i, intermediateKey := range keys {
nextTree, exists := subtree.values[intermediateKey] nextTree, exists := subtree.values[intermediateKey]
if !exists { if !exists {
tree := newTree() tree := newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col})
tree.position = pos tree.position = pos
tree.inline = subtree.inline
subtree.values[intermediateKey] = tree subtree.values[intermediateKey] = tree
nextTree = tree nextTree = tree
} }
@@ -251,8 +464,8 @@ func (t *Tree) createSubTree(keys []string, pos Position) error {
return nil return nil
} }
// LoadReader creates a Tree from any io.Reader. // LoadBytes creates a Tree from a []byte.
func LoadReader(reader io.Reader) (tree *Tree, err error) { func LoadBytes(b []byte) (tree *Tree, err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
if _, ok := r.(runtime.Error); ok { if _, ok := r.(runtime.Error); ok {
@@ -261,13 +474,52 @@ func LoadReader(reader io.Reader) (tree *Tree, err error) {
err = errors.New(r.(string)) err = errors.New(r.(string))
} }
}() }()
tree = parseToml(lexToml(reader))
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 return
} }
// Load creates a Tree from a string. // Load creates a Tree from a string.
func Load(content string) (tree *Tree, err error) { func Load(content string) (tree *Tree, err error) {
return LoadReader(strings.NewReader(content)) return LoadBytes([]byte(content))
} }
// LoadFile creates a Tree from a file. // LoadFile creates a Tree from a file.
+155
View File
@@ -3,6 +3,7 @@
package toml package toml
import ( import (
"reflect"
"testing" "testing"
) )
@@ -39,6 +40,41 @@ func TestTomlGet(t *testing.T) {
} }
} }
func TestTomlGetArray(t *testing.T) {
tree, _ := Load(`
[test]
key = ["one", "two"]
key2 = [true, false, false]
key3 = [1.5,2.5]
`)
if tree.GetArray("") != tree {
t.Errorf("GetArray should return the tree itself when given an empty path")
}
expect := []string{"one", "two"}
actual := tree.GetArray("test.key").([]string)
if !reflect.DeepEqual(actual, expect) {
t.Errorf("GetArray should return the []string value")
}
expect2 := []bool{true, false, false}
actual2 := tree.GetArray("test.key2").([]bool)
if !reflect.DeepEqual(actual2, expect2) {
t.Errorf("GetArray should return the []bool value")
}
expect3 := []float64{1.5, 2.5}
actual3 := tree.GetArray("test.key3").([]float64)
if !reflect.DeepEqual(actual3, expect3) {
t.Errorf("GetArray should return the []float64 value")
}
if tree.GetArray(`\`) != nil {
t.Errorf("should return nil when the key is malformed")
}
}
func TestTomlGetDefault(t *testing.T) { func TestTomlGetDefault(t *testing.T) {
tree, _ := Load(` tree, _ := Load(`
[test] [test]
@@ -69,6 +105,60 @@ func TestTomlHasPath(t *testing.T) {
} }
} }
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) { func TestTomlGetPath(t *testing.T) {
node := newTree() node := newTree()
//TODO: set other node data //TODO: set other node data
@@ -94,6 +184,51 @@ func TestTomlGetPath(t *testing.T) {
} }
} }
func TestTomlGetArrayPath(t *testing.T) {
for idx, item := range []struct {
Name string
Path []string
Make func() (tree *Tree, expected interface{})
}{
{
Name: "empty",
Path: []string{},
Make: func() (tree *Tree, expected interface{}) {
tree = newTree()
expected = tree
return
},
},
{
Name: "int64",
Path: []string{"a"},
Make: func() (tree *Tree, expected interface{}) {
var err error
tree, err = Load(`a = [1,2,3]`)
if err != nil {
panic(err)
}
expected = []int64{1, 2, 3}
return
},
},
} {
t.Run(item.Name, func(t *testing.T) {
tree, expected := item.Make()
result := tree.GetArrayPath(item.Path)
if !reflect.DeepEqual(result, expected) {
t.Errorf("GetArrayPath[%d] %v - expected %#v, got %#v instead.", idx, item.Path, expected, result)
}
})
}
tree, _ := Load("[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6")
if tree.GetArrayPath([]string{"whatever"}) != nil {
t.Error("GetArrayPath should return nil when the key does not exist")
}
}
func TestTomlFromMap(t *testing.T) { func TestTomlFromMap(t *testing.T) {
simpleMap := map[string]interface{}{"hello": 42} simpleMap := map[string]interface{}{"hello": 42}
tree, err := TreeFromMap(simpleMap) tree, err := TreeFromMap(simpleMap)
@@ -104,3 +239,23 @@ func TestTomlFromMap(t *testing.T) {
t.Fatal("hello should be 42, not", tree.Get("hello")) 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)
}
}
}
+117
View File
@@ -0,0 +1,117 @@
// This is a support file for toml_testgen_test.go
package toml
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"
"time"
)
func testgenInvalid(t *testing.T, input string) {
t.Logf("Input TOML:\n%s", input)
tree, err := Load(input)
if err != nil {
return
}
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.Logf("Input TOML:\n%s", input)
tree, err := Load(input)
if err != nil {
t.Fatalf("failed parsing toml: %s", err)
}
typedTree := testgenTranslate(*tree)
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(typedTree); err != nil {
t.Fatalf("failed translating to JSON: %s", err)
}
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)
}
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)
}
if !reflect.DeepEqual(jsonExpected, jsonTest) {
t.Logf("Diff:\n%#+v\n%#+v", jsonExpected, jsonTest)
t.Fatal("parsed TOML tree is different than expected structure")
}
}
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)
}
panic(fmt.Sprintf("Unknown type: %T", tomlData))
}
func testgenTag(typeName string, data interface{}) map[string]interface{} {
return map[string]interface{}{
"type": typeName,
"value": data,
}
}
+928
View File
@@ -0,0 +1,928 @@
// Generated by tomltestgen for toml-test ref 39e37e6 on 2019-03-19T23:58:45-07:00
package toml
import (
"testing"
)
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"
[fruit.type]
apple = "yes"`
testgenInvalid(t, input)
}
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]`
testgenInvalid(t, input)
}
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" +
"# comply with the TOML spec: \"Each double-bracketed sub-table will belong to \n" +
"# the most *recently* defined table element *above* it.\"\n" +
"#\n" +
"# This is in contrast to the *valid* test, table-array-implicit where\n" +
"# `[[albums.songs]]` works by itself, so long as `[[albums]]` isn't declared\n" +
"# later. (Although, `[albums]` could be.)\n" +
"[[albums.songs]]\n" +
"name = \"Glory Days\"\n" +
"\n" +
"[[albums]]\n" +
"name = \"Born in the USA\"\n"
testgenInvalid(t, input)
}
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"
]`
testgenInvalid(t, input)
}
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"
]`
testgenInvalid(t, input)
}
func TestInvalidTextInArray(t *testing.T) {
input := `array = [
"Entry 1",
I don't belong,
"Entry 2",
]`
testgenInvalid(t, input)
}
func TestValidArrayEmpty(t *testing.T) {
input := `thevoid = [[[[[]]]]]`
jsonRef := `{
"thevoid": { "type": "array", "value": [
{"type": "array", "value": [
{"type": "array", "value": [
{"type": "array", "value": [
{"type": "array", "value": []}
]}
]}
]}
]}
}`
testgenValid(t, input, jsonRef)
}
func TestValidArrayNospaces(t *testing.T) {
input := `ints = [1,2,3]`
jsonRef := `{
"ints": {
"type": "array",
"value": [
{"type": "integer", "value": "1"},
{"type": "integer", "value": "2"},
{"type": "integer", "value": "3"}
]
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidArraysHetergeneous(t *testing.T) {
input := `mixed = [[1, 2], ["a", "b"], [1.1, 2.1]]`
jsonRef := `{
"mixed": {
"type": "array",
"value": [
{"type": "array", "value": [
{"type": "integer", "value": "1"},
{"type": "integer", "value": "2"}
]},
{"type": "array", "value": [
{"type": "string", "value": "a"},
{"type": "string", "value": "b"}
]},
{"type": "array", "value": [
{"type": "float", "value": "1.1"},
{"type": "float", "value": "2.1"}
]}
]
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidArraysNested(t *testing.T) {
input := `nest = [["a"], ["b"]]`
jsonRef := `{
"nest": {
"type": "array",
"value": [
{"type": "array", "value": [
{"type": "string", "value": "a"}
]},
{"type": "array", "value": [
{"type": "string", "value": "b"}
]}
]
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidArrays(t *testing.T) {
input := `ints = [1, 2, 3]
floats = [1.1, 2.1, 3.1]
strings = ["a", "b", "c"]
dates = [
1987-07-05T17:45:00Z,
1979-05-27T07:32:00Z,
2006-06-01T11:00:00Z,
]`
jsonRef := `{
"ints": {
"type": "array",
"value": [
{"type": "integer", "value": "1"},
{"type": "integer", "value": "2"},
{"type": "integer", "value": "3"}
]
},
"floats": {
"type": "array",
"value": [
{"type": "float", "value": "1.1"},
{"type": "float", "value": "2.1"},
{"type": "float", "value": "3.1"}
]
},
"strings": {
"type": "array",
"value": [
{"type": "string", "value": "a"},
{"type": "string", "value": "b"},
{"type": "string", "value": "c"}
]
},
"dates": {
"type": "array",
"value": [
{"type": "datetime", "value": "1987-07-05T17:45:00Z"},
{"type": "datetime", "value": "1979-05-27T07:32:00Z"},
{"type": "datetime", "value": "2006-06-01T11:00:00Z"}
]
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidBool(t *testing.T) {
input := `t = true
f = false`
jsonRef := `{
"f": {"type": "bool", "value": "false"},
"t": {"type": "bool", "value": "true"}
}`
testgenValid(t, input, jsonRef)
}
func TestValidCommentsEverywhere(t *testing.T) {
input := `# Top comment.
# Top comment.
# Top comment.
# [no-extraneous-groups-please]
[group] # Comment
answer = 42 # Comment
# no-extraneous-keys-please = 999
# Inbetween comment.
more = [ # Comment
# What about multiple # comments?
# Can you handle it?
#
# Evil.
# Evil.
42, 42, # Comments within arrays are fun.
# What about multiple # comments?
# Can you handle it?
#
# Evil.
# Evil.
# ] Did I fool you?
] # Hopefully not.`
jsonRef := `{
"group": {
"answer": {"type": "integer", "value": "42"},
"more": {
"type": "array",
"value": [
{"type": "integer", "value": "42"},
{"type": "integer", "value": "42"}
]
}
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidDatetime(t *testing.T) {
input := `bestdayever = 1987-07-05T17:45:00Z`
jsonRef := `{
"bestdayever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"}
}`
testgenValid(t, input, jsonRef)
}
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]
boring = false
perfection = [6, 28, 496]`
jsonRef := `{
"best-day-ever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"},
"numtheory": {
"boring": {"type": "bool", "value": "false"},
"perfection": {
"type": "array",
"value": [
{"type": "integer", "value": "6"},
{"type": "integer", "value": "28"},
{"type": "integer", "value": "496"}
]
}
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidFloat(t *testing.T) {
input := `pi = 3.14
negpi = -3.14`
jsonRef := `{
"pi": {"type": "float", "value": "3.14"},
"negpi": {"type": "float", "value": "-3.14"}
}`
testgenValid(t, input, jsonRef)
}
func TestValidImplicitAndExplicitAfter(t *testing.T) {
input := `[a.b.c]
answer = 42
[a]
better = 43`
jsonRef := `{
"a": {
"better": {"type": "integer", "value": "43"},
"b": {
"c": {
"answer": {"type": "integer", "value": "42"}
}
}
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidImplicitAndExplicitBefore(t *testing.T) {
input := `[a]
better = 43
[a.b.c]
answer = 42`
jsonRef := `{
"a": {
"better": {"type": "integer", "value": "43"},
"b": {
"c": {
"answer": {"type": "integer", "value": "42"}
}
}
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidImplicitGroups(t *testing.T) {
input := `[a.b.c]
answer = 42`
jsonRef := `{
"a": {
"b": {
"c": {
"answer": {"type": "integer", "value": "42"}
}
}
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidInteger(t *testing.T) {
input := `answer = 42
neganswer = -42`
jsonRef := `{
"answer": {"type": "integer", "value": "42"},
"neganswer": {"type": "integer", "value": "-42"}
}`
testgenValid(t, input, jsonRef)
}
func TestValidKeyEqualsNospace(t *testing.T) {
input := `answer=42`
jsonRef := `{
"answer": {"type": "integer", "value": "42"}
}`
testgenValid(t, input, jsonRef)
}
func TestValidKeySpace(t *testing.T) {
input := `"a b" = 1`
jsonRef := `{
"a b": {"type": "integer", "value": "1"}
}`
testgenValid(t, input, jsonRef)
}
func TestValidKeySpecialChars(t *testing.T) {
input := "\"~!@$^&*()_+-`1234567890[]|/?><.,;:'\" = 1\n"
jsonRef := "{\n" +
" \"~!@$^&*()_+-`1234567890[]|/?><.,;:'\": {\n" +
" \"type\": \"integer\", \"value\": \"1\"\n" +
" }\n" +
"}\n"
testgenValid(t, input, jsonRef)
}
func TestValidLongFloat(t *testing.T) {
input := `longpi = 3.141592653589793
neglongpi = -3.141592653589793`
jsonRef := `{
"longpi": {"type": "float", "value": "3.141592653589793"},
"neglongpi": {"type": "float", "value": "-3.141592653589793"}
}`
testgenValid(t, input, jsonRef)
}
func TestValidLongInteger(t *testing.T) {
input := `answer = 9223372036854775807
neganswer = -9223372036854775808`
jsonRef := `{
"answer": {"type": "integer", "value": "9223372036854775807"},
"neganswer": {"type": "integer", "value": "-9223372036854775808"}
}`
testgenValid(t, input, jsonRef)
}
func TestValidMultilineString(t *testing.T) {
input := `multiline_empty_one = """"""
multiline_empty_two = """
"""
multiline_empty_three = """\
"""
multiline_empty_four = """\
\
\
"""
equivalent_one = "The quick brown fox jumps over the lazy dog."
equivalent_two = """
The quick brown \
fox jumps over \
the lazy dog."""
equivalent_three = """\
The quick brown \
fox jumps over \
the lazy dog.\
"""`
jsonRef := `{
"multiline_empty_one": {
"type": "string",
"value": ""
},
"multiline_empty_two": {
"type": "string",
"value": ""
},
"multiline_empty_three": {
"type": "string",
"value": ""
},
"multiline_empty_four": {
"type": "string",
"value": ""
},
"equivalent_one": {
"type": "string",
"value": "The quick brown fox jumps over the lazy dog."
},
"equivalent_two": {
"type": "string",
"value": "The quick brown fox jumps over the lazy dog."
},
"equivalent_three": {
"type": "string",
"value": "The quick brown fox jumps over the lazy dog."
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidRawMultilineString(t *testing.T) {
input := `oneline = '''This string has a ' quote character.'''
firstnl = '''
This string has a ' quote character.'''
multiline = '''
This string
has ' a quote character
and more than
one newline
in it.'''`
jsonRef := `{
"oneline": {
"type": "string",
"value": "This string has a ' quote character."
},
"firstnl": {
"type": "string",
"value": "This string has a ' quote character."
},
"multiline": {
"type": "string",
"value": "This string\nhas ' a quote character\nand more than\none newline\nin it."
}
}`
testgenValid(t, input, jsonRef)
}
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.'
formfeed = 'This string has a \f form feed character.'
carriage = 'This string has a \r carriage return character.'
slash = 'This string has a \/ slash character.'
backslash = 'This string has a \\ backslash character.'`
jsonRef := `{
"backspace": {
"type": "string",
"value": "This string has a \\b backspace character."
},
"tab": {
"type": "string",
"value": "This string has a \\t tab character."
},
"newline": {
"type": "string",
"value": "This string has a \\n new line character."
},
"formfeed": {
"type": "string",
"value": "This string has a \\f form feed character."
},
"carriage": {
"type": "string",
"value": "This string has a \\r carriage return character."
},
"slash": {
"type": "string",
"value": "This string has a \\/ slash character."
},
"backslash": {
"type": "string",
"value": "This string has a \\\\ backslash character."
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidStringEmpty(t *testing.T) {
input := `answer = ""`
jsonRef := `{
"answer": {
"type": "string",
"value": ""
}
}`
testgenValid(t, input, jsonRef)
}
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."
formfeed = "This string has a \f form feed character."
carriage = "This string has a \r carriage return character."
quote = "This string has a \" quote character."
backslash = "This string has a \\ backslash character."
notunicode1 = "This string does not have a unicode \\u escape."
notunicode2 = "This string does not have a unicode \u005Cu escape."
notunicode3 = "This string does not have a unicode \\u0075 escape."
notunicode4 = "This string does not have a unicode \\\u0075 escape."`
jsonRef := `{
"backspace": {
"type": "string",
"value": "This string has a \u0008 backspace character."
},
"tab": {
"type": "string",
"value": "This string has a \u0009 tab character."
},
"newline": {
"type": "string",
"value": "This string has a \u000A new line character."
},
"formfeed": {
"type": "string",
"value": "This string has a \u000C form feed character."
},
"carriage": {
"type": "string",
"value": "This string has a \u000D carriage return character."
},
"quote": {
"type": "string",
"value": "This string has a \u0022 quote character."
},
"backslash": {
"type": "string",
"value": "This string has a \u005C backslash character."
},
"notunicode1": {
"type": "string",
"value": "This string does not have a unicode \\u escape."
},
"notunicode2": {
"type": "string",
"value": "This string does not have a unicode \u005Cu escape."
},
"notunicode3": {
"type": "string",
"value": "This string does not have a unicode \\u0075 escape."
},
"notunicode4": {
"type": "string",
"value": "This string does not have a unicode \\\u0075 escape."
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidStringSimple(t *testing.T) {
input := `answer = "You are not drinking enough whisky."`
jsonRef := `{
"answer": {
"type": "string",
"value": "You are not drinking enough whisky."
}
}`
testgenValid(t, input, jsonRef)
}
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 := `{
"pound": {"type": "string", "value": "We see no # comments here."},
"poundcomment": {
"type": "string",
"value": "But there are # some comments here."
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidTableArrayImplicit(t *testing.T) {
input := `[[albums.songs]]
name = "Glory Days"`
jsonRef := `{
"albums": {
"songs": [
{"name": {"type": "string", "value": "Glory Days"}}
]
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidTableArrayMany(t *testing.T) {
input := `[[people]]
first_name = "Bruce"
last_name = "Springsteen"
[[people]]
first_name = "Eric"
last_name = "Clapton"
[[people]]
first_name = "Bob"
last_name = "Seger"`
jsonRef := `{
"people": [
{
"first_name": {"type": "string", "value": "Bruce"},
"last_name": {"type": "string", "value": "Springsteen"}
},
{
"first_name": {"type": "string", "value": "Eric"},
"last_name": {"type": "string", "value": "Clapton"}
},
{
"first_name": {"type": "string", "value": "Bob"},
"last_name": {"type": "string", "value": "Seger"}
}
]
}`
testgenValid(t, input, jsonRef)
}
func TestValidTableArrayNest(t *testing.T) {
input := `[[albums]]
name = "Born to Run"
[[albums.songs]]
name = "Jungleland"
[[albums.songs]]
name = "Meeting Across the River"
[[albums]]
name = "Born in the USA"
[[albums.songs]]
name = "Glory Days"
[[albums.songs]]
name = "Dancing in the Dark"`
jsonRef := `{
"albums": [
{
"name": {"type": "string", "value": "Born to Run"},
"songs": [
{"name": {"type": "string", "value": "Jungleland"}},
{"name": {"type": "string", "value": "Meeting Across the River"}}
]
},
{
"name": {"type": "string", "value": "Born in the USA"},
"songs": [
{"name": {"type": "string", "value": "Glory Days"}},
{"name": {"type": "string", "value": "Dancing in the Dark"}}
]
}
]
}`
testgenValid(t, input, jsonRef)
}
func TestValidTableArrayOne(t *testing.T) {
input := `[[people]]
first_name = "Bruce"
last_name = "Springsteen"`
jsonRef := `{
"people": [
{
"first_name": {"type": "string", "value": "Bruce"},
"last_name": {"type": "string", "value": "Springsteen"}
}
]
}`
testgenValid(t, input, jsonRef)
}
func TestValidTableEmpty(t *testing.T) {
input := `[a]`
jsonRef := `{
"a": {}
}`
testgenValid(t, input, jsonRef)
}
func TestValidTableSubEmpty(t *testing.T) {
input := `[a]
[a.b]`
jsonRef := `{
"a": { "b": {} }
}`
testgenValid(t, input, jsonRef)
}
func TestValidTableWhitespace(t *testing.T) {
input := `["valid key"]`
jsonRef := `{
"valid key": {}
}`
testgenValid(t, input, jsonRef)
}
func TestValidTableWithPound(t *testing.T) {
input := `["key#group"]
answer = 42`
jsonRef := `{
"key#group": {
"answer": {"type": "integer", "value": "42"}
}
}`
testgenValid(t, input, jsonRef)
}
func TestValidUnicodeEscape(t *testing.T) {
input := `answer4 = "\u03B4"
answer8 = "\U000003B4"`
jsonRef := `{
"answer4": {"type": "string", "value": "\u03B4"},
"answer8": {"type": "string", "value": "\u03B4"}
}`
testgenValid(t, input, jsonRef)
}
func TestValidUnicodeLiteral(t *testing.T) {
input := `answer = "δ"`
jsonRef := `{
"answer": {"type": "string", "value": "δ"}
}`
testgenValid(t, input, jsonRef)
}
+71
View File
@@ -0,0 +1,71 @@
package toml
// PubTOMLValue wrapping tomlValue in order to access all properties from outside.
type PubTOMLValue = tomlValue
func (ptv *PubTOMLValue) Value() interface{} {
return ptv.value
}
func (ptv *PubTOMLValue) Comment() string {
return ptv.comment
}
func (ptv *PubTOMLValue) Commented() bool {
return ptv.commented
}
func (ptv *PubTOMLValue) Multiline() bool {
return ptv.multiline
}
func (ptv *PubTOMLValue) Position() Position {
return ptv.position
}
func (ptv *PubTOMLValue) SetValue(v interface{}) {
ptv.value = v
}
func (ptv *PubTOMLValue) SetComment(s string) {
ptv.comment = s
}
func (ptv *PubTOMLValue) SetCommented(c bool) {
ptv.commented = c
}
func (ptv *PubTOMLValue) SetMultiline(m bool) {
ptv.multiline = m
}
func (ptv *PubTOMLValue) SetPosition(p Position) {
ptv.position = p
}
// PubTree wrapping Tree in order to access all properties from outside.
type PubTree = Tree
func (pt *PubTree) Values() map[string]interface{} {
return pt.values
}
func (pt *PubTree) Comment() string {
return pt.comment
}
func (pt *PubTree) Commented() bool {
return pt.commented
}
func (pt *PubTree) Inline() bool {
return pt.inline
}
func (pt *PubTree) SetValues(v map[string]interface{}) {
pt.values = v
}
func (pt *PubTree) SetComment(c string) {
pt.comment = c
}
func (pt *PubTree) SetCommented(c bool) {
pt.commented = c
}
func (pt *PubTree) SetInline(i bool) {
pt.inline = i
}
+16 -3
View File
@@ -57,6 +57,19 @@ func simpleValueCoercion(object interface{}) (interface{}, error) {
return float64(original), nil return float64(original), nil
case fmt.Stringer: case fmt.Stringer:
return original.String(), nil return original.String(), nil
case []interface{}:
value := reflect.ValueOf(original)
length := value.Len()
arrayValue := reflect.MakeSlice(value.Type(), 0, length)
for i := 0; i < length; i++ {
val := value.Index(i).Interface()
simpleValue, err := simpleValueCoercion(val)
if err != nil {
return nil, err
}
arrayValue = reflect.Append(arrayValue, reflect.ValueOf(simpleValue))
}
return arrayValue.Interface(), nil
default: default:
return nil, fmt.Errorf("cannot convert type %T to Tree", object) return nil, fmt.Errorf("cannot convert type %T to Tree", object)
} }
@@ -104,7 +117,7 @@ func sliceToTree(object interface{}) (interface{}, error) {
} }
arrayValue = reflect.Append(arrayValue, reflect.ValueOf(simpleValue)) arrayValue = reflect.Append(arrayValue, reflect.ValueOf(simpleValue))
} }
return &tomlValue{arrayValue.Interface(), Position{}}, nil return &tomlValue{value: arrayValue.Interface(), position: Position{}}, nil
} }
func toTree(object interface{}) (interface{}, error) { func toTree(object interface{}) (interface{}, error) {
@@ -127,7 +140,7 @@ func toTree(object interface{}) (interface{}, error) {
} }
values[key.String()] = newValue values[key.String()] = newValue
} }
return &Tree{values, Position{}}, nil return &Tree{values: values, position: Position{}}, nil
} }
if value.Kind() == reflect.Array || value.Kind() == reflect.Slice { if value.Kind() == reflect.Array || value.Kind() == reflect.Slice {
@@ -138,5 +151,5 @@ func toTree(object interface{}) (interface{}, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &tomlValue{simpleValue, Position{}}, nil return &tomlValue{value: simpleValue, position: Position{}}, nil
} }
+120 -3
View File
@@ -1,6 +1,7 @@
package toml package toml
import ( import (
"reflect"
"strconv" "strconv"
"testing" "testing"
"time" "time"
@@ -60,7 +61,7 @@ func TestTreeCreateToTree(t *testing.T) {
}, },
"array": []string{"a", "b", "c"}, "array": []string{"a", "b", "c"},
"array_uint": []uint{uint(1), uint(2)}, "array_uint": []uint{uint(1), uint(2)},
"array_table": []map[string]interface{}{map[string]interface{}{"sub_map": 52}}, "array_table": []map[string]interface{}{{"sub_map": 52}},
"array_times": []time.Time{time.Now(), time.Now()}, "array_times": []time.Time{time.Now(), time.Now()},
"map_times": map[string]time.Time{"now": time.Now()}, "map_times": map[string]time.Time{"now": time.Now()},
"custom_string_map_key": map[customString]interface{}{customString("custom"): "custom"}, "custom_string_map_key": map[customString]interface{}{customString("custom"): "custom"},
@@ -97,7 +98,7 @@ func TestTreeCreateToTreeInvalidArrayMemberType(t *testing.T) {
} }
func TestTreeCreateToTreeInvalidTableGroupType(t *testing.T) { func TestTreeCreateToTreeInvalidTableGroupType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": []map[string]interface{}{map[string]interface{}{"hello": t}}}) _, err := TreeFromMap(map[string]interface{}{"foo": []map[string]interface{}{{"hello": t}}})
expected := "cannot convert type *testing.T to Tree" expected := "cannot convert type *testing.T to Tree"
if err.Error() != expected { if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error()) t.Fatalf("expected error %s, got %s", expected, err.Error())
@@ -105,7 +106,7 @@ func TestTreeCreateToTreeInvalidTableGroupType(t *testing.T) {
} }
func TestRoundTripArrayOfTables(t *testing.T) { func TestRoundTripArrayOfTables(t *testing.T) {
orig := "\n[[stuff]]\n name = \"foo\"\n things = [\"a\",\"b\"]\n" orig := "\n[[stuff]]\n name = \"foo\"\n things = [\"a\", \"b\"]\n"
tree, err := Load(orig) tree, err := Load(orig)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
@@ -124,3 +125,119 @@ func TestRoundTripArrayOfTables(t *testing.T) {
t.Errorf("want:\n%s\ngot:\n%s", want, got) t.Errorf("want:\n%s\ngot:\n%s", want, got)
} }
} }
func TestTomlSliceOfSlice(t *testing.T) {
tree, err := Load(` hosts=[["10.1.0.107:9092","10.1.0.107:9093", "192.168.0.40:9094"] ] `)
m := tree.ToMap()
tree, err = TreeFromMap(m)
if err != nil {
t.Error("should not error", err)
}
type Struct struct {
Hosts [][]string
}
var actual Struct
tree.Unmarshal(&actual)
expected := Struct{Hosts: [][]string{[]string{"10.1.0.107:9092", "10.1.0.107:9093", "192.168.0.40:9094"}}}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual)
}
}
func TestTomlSliceOfSliceOfSlice(t *testing.T) {
tree, err := Load(` hosts=[[["10.1.0.107:9092","10.1.0.107:9093", "192.168.0.40:9094"] ]] `)
m := tree.ToMap()
tree, err = TreeFromMap(m)
if err != nil {
t.Error("should not error", err)
}
type Struct struct {
Hosts [][][]string
}
var actual Struct
tree.Unmarshal(&actual)
expected := Struct{Hosts: [][][]string{[][]string{[]string{"10.1.0.107:9092", "10.1.0.107:9093", "192.168.0.40:9094"}}}}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual)
}
}
func TestTomlSliceOfSliceInt(t *testing.T) {
tree, err := Load(` hosts=[[1,2,3],[4,5,6] ] `)
m := tree.ToMap()
tree, err = TreeFromMap(m)
if err != nil {
t.Error("should not error", err)
}
type Struct struct {
Hosts [][]int
}
var actual Struct
err = tree.Unmarshal(&actual)
if err != nil {
t.Error("should not error", err)
}
expected := Struct{Hosts: [][]int{[]int{1, 2, 3}, []int{4, 5, 6}}}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual)
}
}
func TestTomlSliceOfSliceInt64(t *testing.T) {
tree, err := Load(` hosts=[[1,2,3],[4,5,6] ] `)
m := tree.ToMap()
tree, err = TreeFromMap(m)
if err != nil {
t.Error("should not error", err)
}
type Struct struct {
Hosts [][]int64
}
var actual Struct
err = tree.Unmarshal(&actual)
if err != nil {
t.Error("should not error", err)
}
expected := Struct{Hosts: [][]int64{[]int64{1, 2, 3}, []int64{4, 5, 6}}}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual)
}
}
func TestTomlSliceOfSliceInt64FromMap(t *testing.T) {
tree, err := TreeFromMap(map[string]interface{}{"hosts": [][]interface{}{[]interface{}{int32(1), int8(2), 3}}})
if err != nil {
t.Error("should not error", err)
}
type Struct struct {
Hosts [][]int64
}
var actual Struct
err = tree.Unmarshal(&actual)
if err != nil {
t.Error("should not error", err)
}
expected := Struct{Hosts: [][]int64{[]int64{1, 2, 3}}}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual)
}
}
func TestTomlSliceOfSliceError(t *testing.T) { // make Codecov happy
_, err := TreeFromMap(map[string]interface{}{"hosts": [][]interface{}{[]interface{}{1, 2, []struct{}{}}}})
expected := "cannot convert type []struct {} to Tree"
if err.Error() != expected {
t.Fatalf("unexpected error: %s", err)
}
}
+402 -74
View File
@@ -4,6 +4,8 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"math"
"math/big"
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
@@ -11,50 +13,165 @@ import (
"time" "time"
) )
// encodes a string to a TOML-compliant string value type valueComplexity int
func encodeTomlString(value string) string {
result := "" const (
for _, rr := range value { 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, commented string) string {
var b bytes.Buffer
adjacentQuoteCount := 0
b.WriteString(commented)
for i, rr := range value {
if rr != '"' {
adjacentQuoteCount = 0
} else {
adjacentQuoteCount++
}
switch rr { switch rr {
case '\b': case '\b':
result += "\\b" b.WriteString(`\b`)
case '\t': case '\t':
result += "\\t" b.WriteString("\t")
case '\n': case '\n':
result += "\\n" b.WriteString("\n" + commented)
case '\f': case '\f':
result += "\\f" b.WriteString(`\f`)
case '\r': case '\r':
result += "\\r" b.WriteString("\r")
case '"': case '"':
result += "\\\"" if adjacentQuoteCount >= 3 || i == len(value)-1 {
adjacentQuoteCount = 0
b.WriteString(`\"`)
} else {
b.WriteString(`"`)
}
case '\\': case '\\':
result += "\\\\" b.WriteString(`\`)
default: default:
intRr := uint16(rr) intRr := uint16(rr)
if intRr < 0x001F { if intRr < 0x001F {
result += fmt.Sprintf("\\u%0.4X", intRr) b.WriteString(fmt.Sprintf("\\u%0.4X", intRr))
} else { } else {
result += string(rr) b.WriteRune(rr)
} }
} }
} }
return result return b.String()
} }
func tomlValueStringRepresentation(v interface{}) (string, error) { // 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 tomlTreeStringRepresentation(t *Tree, ord MarshalOrder) (string, error) {
var orderedVals []sortNode
switch ord {
case OrderPreserve:
orderedVals = sortByLines(t)
default:
orderedVals = sortAlphabetical(t)
}
var values []string
for _, node := range orderedVals {
k := node.key
v := t.values[k]
repr, err := tomlValueStringRepresentation(v, "", "", ord, false)
if err != nil {
return "", err
}
values = append(values, quoteKeyIfNeeded(k)+" = "+repr)
}
return "{ " + strings.Join(values, ", ") + " }", nil
}
func tomlValueStringRepresentation(v interface{}, commented string, indent string, ord MarshalOrder, arraysOneElementPerLine bool) (string, error) {
// this interface check is added to dereference the change made in the writeTo function.
// That change was made to allow this function to see formatting options.
tv, ok := v.(*tomlValue)
if ok {
v = tv.value
} else {
tv = &tomlValue{}
}
switch value := v.(type) { switch value := v.(type) {
case uint64: case uint64:
return strconv.FormatUint(value, 10), nil return strconv.FormatUint(value, 10), nil
case int64: case int64:
return strconv.FormatInt(value, 10), nil return strconv.FormatInt(value, 10), nil
case float64: case float64:
return strconv.FormatFloat(value, 'f', -1, 32), nil // 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: case string:
if tv.multiline {
if tv.literal {
b := strings.Builder{}
b.WriteString("'''\n")
b.Write([]byte(value))
b.WriteString("\n'''")
return b.String(), nil
} else {
return "\"\"\"\n" + encodeMultilineTomlString(value, commented) + "\"\"\"", nil
}
}
return "\"" + encodeTomlString(value) + "\"", nil return "\"" + encodeTomlString(value) + "\"", nil
case []byte: case []byte:
b, _ := v.([]byte) b, _ := v.([]byte)
return tomlValueStringRepresentation(string(b)) return string(b), nil
case bool: case bool:
if value { if value {
return "true", nil return "true", nil
@@ -62,6 +179,14 @@ func tomlValueStringRepresentation(v interface{}) (string, error) {
return "false", nil return "false", nil
case time.Time: case time.Time:
return value.Format(time.RFC3339), nil 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 *Tree:
return tomlTreeStringRepresentation(value, ord)
case nil: case nil:
return "", nil return "", nil
} }
@@ -69,90 +194,238 @@ func tomlValueStringRepresentation(v interface{}) (string, error) {
rv := reflect.ValueOf(v) rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Slice { if rv.Kind() == reflect.Slice {
values := []string{} var values []string
for i := 0; i < rv.Len(); i++ { for i := 0; i < rv.Len(); i++ {
item := rv.Index(i).Interface() item := rv.Index(i).Interface()
itemRepr, err := tomlValueStringRepresentation(item) itemRepr, err := tomlValueStringRepresentation(item, commented, indent, ord, arraysOneElementPerLine)
if err != nil { if err != nil {
return "", err return "", err
} }
values = append(values, itemRepr) values = append(values, itemRepr)
} }
return "[" + strings.Join(values, ",") + "]", nil 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(commented + value)
stringBuffer.WriteString(`,`)
stringBuffer.WriteString("\n")
}
stringBuffer.WriteString(indent + commented + "]")
return stringBuffer.String(), nil
}
return "[" + strings.Join(values, ", ") + "]", nil
} }
return "", fmt.Errorf("unsupported value type %T: %v", v, v) return "", fmt.Errorf("unsupported value type %T: %v", v, v)
} }
func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64) (int64, error) { func getTreeArrayLine(trees []*Tree) (line int) {
simpleValuesKeys := make([]string, 0) // Prevent returning 0 for empty trees
complexValuesKeys := make([]string, 0) line = int(^uint(0) >> 1)
// get lowest line number >= 0
for _, tv := range trees {
if tv.position.Line < line || line == 0 {
line = tv.position.Line
}
}
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 { for k := range t.values {
v := t.values[k] v := t.values[k]
switch v.(type) { switch v.(type) {
case *Tree, []*Tree: case *Tree, []*Tree:
complexValuesKeys = append(complexValuesKeys, k) node = sortNode{key: k, complexity: valueComplex}
compVals = append(compVals, node.key)
default: default:
simpleValuesKeys = append(simpleValuesKeys, k) node = sortNode{key: k, complexity: valueSimple}
simpVals = append(simpVals, node.key)
} }
vals = append(vals, node)
m[node.key] = node
} }
sort.Strings(simpleValuesKeys) // Simples first to match previous implementation
sort.Strings(complexValuesKeys) sort.Strings(simpVals)
i := 0
for _, k := range simpleValuesKeys { for _, key := range simpVals {
v, ok := t.values[k].(*tomlValue) vals[i] = m[key]
if !ok { i++
return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
}
repr, err := tomlValueStringRepresentation(v.value)
if err != nil {
return bytesCount, err
}
kvRepr := fmt.Sprintf("%s%s = %s\n", indent, k, repr)
writtenBytesCount, err := w.Write([]byte(kvRepr))
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
}
} }
for _, k := range complexValuesKeys { sort.Strings(compVals)
v := t.values[k] for _, key := range compVals {
vals[i] = m[key]
i++
}
combinedKey := k return vals
if keyspace != "" { }
combinedKey = keyspace + "." + combinedKey
}
switch node := v.(type) { func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool) (int64, error) {
// node has to be of those two types given how keys are sorted above return t.writeToOrdered(w, indent, keyspace, bytesCount, arraysOneElementPerLine, OrderAlphabetical, " ", false)
case *Tree: }
tableName := fmt.Sprintf("\n%s[%s]\n", indent, combinedKey)
writtenBytesCount, err := w.Write([]byte(tableName)) func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool, ord MarshalOrder, indentString string, parentCommented bool) (int64, error) {
bytesCount += int64(writtenBytesCount) var orderedVals []sortNode
if err != nil {
return bytesCount, err 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 := quoteKeyIfNeeded(k)
if keyspace != "" {
combinedKey = keyspace + "." + combinedKey
} }
bytesCount, err = node.writeTo(w, indent+" ", combinedKey, bytesCount)
if err != nil { switch node := v.(type) {
return bytesCount, err // node has to be of those two types given how keys are sorted above
} case *Tree:
case []*Tree: tv, ok := t.values[k].(*Tree)
for _, subTree := range node { if !ok {
tableArrayName := fmt.Sprintf("\n%s[[%s]]\n", indent, combinedKey) return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
writtenBytesCount, err := w.Write([]byte(tableArrayName)) }
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
}
}
var commented string
if parentCommented || t.commented || tv.commented {
commented = "# "
}
writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[", combinedKey, "]\n")
bytesCount += int64(writtenBytesCount) bytesCount += int64(writtenBytesCount)
if err != nil { if err != nil {
return bytesCount, err return bytesCount, err
} }
bytesCount, err = node.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, parentCommented || t.commented || tv.commented)
bytesCount, err = subTree.writeTo(w, indent+" ", combinedKey, bytesCount)
if err != nil { if err != nil {
return bytesCount, err return bytesCount, err
} }
case []*Tree:
for _, subTree := range node {
var commented string
if parentCommented || t.commented || subTree.commented {
commented = "# "
}
writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[[", combinedKey, "]]\n")
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
}
bytesCount, err = subTree.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, parentCommented || t.commented || subTree.commented)
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])
}
var commented string
if parentCommented || t.commented || v.commented {
commented = "# "
}
repr, err := tomlValueStringRepresentation(v, commented, indent, ord, 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
}
}
quotedKey := quoteKeyIfNeeded(k)
writtenBytesCount, err := writeStrings(w, indent, commented, quotedKey, " = ", repr, "\n")
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
} }
} }
} }
@@ -160,22 +433,59 @@ func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64) (
return bytesCount, nil 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. // 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. // 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) { func (t *Tree) WriteTo(w io.Writer) (int64, error) {
return t.writeTo(w, "", "", 0) return t.writeTo(w, "", "", 0, false)
} }
// ToTomlString generates a human-readable representation of the current tree. // ToTomlString generates a human-readable representation of the current tree.
// Output spans multiple lines, and is suitable for ingest by a TOML parser. // 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. // If the conversion cannot be performed, ToString returns a non-nil error.
func (t *Tree) ToTomlString() (string, error) { func (t *Tree) ToTomlString() (string, error) {
var buf bytes.Buffer b, err := t.Marshal()
_, err := t.WriteTo(&buf)
if err != nil { if err != nil {
return "", err return "", err
} }
return buf.String(), nil return string(b), nil
} }
// String generates a human-readable representation of the current tree. // String generates a human-readable representation of the current tree.
@@ -210,8 +520,26 @@ func (t *Tree) ToMap() map[string]interface{} {
case *Tree: case *Tree:
result[k] = node.ToMap() result[k] = node.ToMap()
case *tomlValue: case *tomlValue:
result[k] = node.value result[k] = tomlValueToGo(node.value)
} }
} }
return result return result
} }
func tomlValueToGo(v interface{}) interface{} {
if tree, ok := v.(*Tree); ok {
return tree.ToMap()
}
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Slice {
return v
}
values := make([]interface{}, rv.Len())
for i := 0; i < rv.Len(); i++ {
item := rv.Index(i).Interface()
values[i] = tomlValueToGo(item)
}
return values
}
+209 -18
View File
@@ -16,26 +16,26 @@ type failingWriter struct {
buffer bytes.Buffer buffer bytes.Buffer
} }
func (f failingWriter) Write(p []byte) (n int, err error) { func (f *failingWriter) Write(p []byte) (n int, err error) {
count := len(p) count := len(p)
toWrite := f.failAt - count + f.written toWrite := f.failAt - (count + f.written)
if toWrite < 0 { if toWrite < 0 {
toWrite = 0 toWrite = 0
} }
if toWrite > count { if toWrite > count {
f.written += count f.written += count
f.buffer.WriteString(string(p)) f.buffer.Write(p)
return count, nil return count, nil
} }
f.buffer.WriteString(string(p[:toWrite])) f.buffer.Write(p[:toWrite])
f.written = f.failAt f.written = f.failAt
return f.written, fmt.Errorf("failingWriter failed after writting %d bytes", f.written) return toWrite, fmt.Errorf("failingWriter failed after writing %d bytes", f.written)
} }
func assertErrorString(t *testing.T, expected string, err error) { func assertErrorString(t *testing.T, expected string, err error) {
expectedErr := errors.New(expected) expectedErr := errors.New(expected)
if err.Error() != expectedErr.Error() { if err == nil || err.Error() != expectedErr.Error() {
t.Errorf("expecting error %s, but got %s instead", expected, err) t.Errorf("expecting error %s, but got %s instead", expected, err)
} }
} }
@@ -161,13 +161,13 @@ func TestTreeWriteToInvalidTreeSimpleValue(t *testing.T) {
} }
func TestTreeWriteToInvalidTreeTomlValue(t *testing.T) { func TestTreeWriteToInvalidTreeTomlValue(t *testing.T) {
tree := Tree{values: map[string]interface{}{"foo": &tomlValue{int8(1), Position{}}}} tree := Tree{values: map[string]interface{}{"foo": &tomlValue{value: int8(1), comment: "", position: Position{}}}}
_, err := tree.ToTomlString() _, err := tree.ToTomlString()
assertErrorString(t, "unsupported value type int8: 1", err) assertErrorString(t, "unsupported value type int8: 1", err)
} }
func TestTreeWriteToInvalidTreeTomlValueArray(t *testing.T) { func TestTreeWriteToInvalidTreeTomlValueArray(t *testing.T) {
tree := Tree{values: map[string]interface{}{"foo": &tomlValue{[]interface{}{int8(1)}, Position{}}}} tree := Tree{values: map[string]interface{}{"foo": &tomlValue{value: int8(1), comment: "", position: Position{}}}}
_, err := tree.ToTomlString() _, err := tree.ToTomlString()
assertErrorString(t, "unsupported value type int8: 1", err) assertErrorString(t, "unsupported value type int8: 1", err)
} }
@@ -175,8 +175,8 @@ func TestTreeWriteToInvalidTreeTomlValueArray(t *testing.T) {
func TestTreeWriteToFailingWriterInSimpleValue(t *testing.T) { func TestTreeWriteToFailingWriterInSimpleValue(t *testing.T) {
toml, _ := Load(`a = 2`) toml, _ := Load(`a = 2`)
writer := failingWriter{failAt: 0, written: 0} writer := failingWriter{failAt: 0, written: 0}
_, err := toml.WriteTo(writer) _, err := toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writting 0 bytes", err) assertErrorString(t, "failingWriter failed after writing 0 bytes", err)
} }
func TestTreeWriteToFailingWriterInTable(t *testing.T) { func TestTreeWriteToFailingWriterInTable(t *testing.T) {
@@ -184,12 +184,12 @@ func TestTreeWriteToFailingWriterInTable(t *testing.T) {
[b] [b]
a = 2`) a = 2`)
writer := failingWriter{failAt: 2, written: 0} writer := failingWriter{failAt: 2, written: 0}
_, err := toml.WriteTo(writer) _, err := toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writting 2 bytes", err) assertErrorString(t, "failingWriter failed after writing 2 bytes", err)
writer = failingWriter{failAt: 13, written: 0} writer = failingWriter{failAt: 13, written: 0}
_, err = toml.WriteTo(writer) _, err = toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writting 13 bytes", err) assertErrorString(t, "failingWriter failed after writing 13 bytes", err)
} }
func TestTreeWriteToFailingWriterInArray(t *testing.T) { func TestTreeWriteToFailingWriterInArray(t *testing.T) {
@@ -197,12 +197,12 @@ func TestTreeWriteToFailingWriterInArray(t *testing.T) {
[[b]] [[b]]
a = 2`) a = 2`)
writer := failingWriter{failAt: 2, written: 0} writer := failingWriter{failAt: 2, written: 0}
_, err := toml.WriteTo(writer) _, err := toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writting 2 bytes", err) assertErrorString(t, "failingWriter failed after writing 2 bytes", err)
writer = failingWriter{failAt: 15, written: 0} writer = failingWriter{failAt: 15, written: 0}
_, err = toml.WriteTo(writer) _, err = toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writting 15 bytes", err) assertErrorString(t, "failingWriter failed after writing 15 bytes", err)
} }
func TestTreeWriteToMapExampleFile(t *testing.T) { func TestTreeWriteToMapExampleFile(t *testing.T) {
@@ -236,6 +236,7 @@ func TestTreeWriteToMapExampleFile(t *testing.T) {
[]interface{}{"gamma", "delta"}, []interface{}{"gamma", "delta"},
[]interface{}{int64(1), int64(2)}, []interface{}{int64(1), int64(2)},
}, },
"score": 4e-08,
}, },
} }
testMaps(t, tree.ToMap(), expected) testMaps(t, tree.ToMap(), expected)
@@ -293,3 +294,193 @@ func TestTreeWriteToMapWithArrayOfInlineTables(t *testing.T) {
treeMap := tree.ToMap() treeMap := tree.ToMap()
testMaps(t, treeMap, expected) testMaps(t, treeMap, expected)
} }
func TestTreeWriteToMapWithTableInMixedArray(t *testing.T) {
tree, _ := Load(`a = [
"foo",
[
"bar",
{baz = "quux"},
],
[
{a = "b"},
{c = "d"},
],
]`)
expected := map[string]interface{}{
"a": []interface{}{
"foo",
[]interface{}{
"bar",
map[string]interface{}{
"baz": "quux",
},
},
[]interface{}{
map[string]interface{}{
"a": "b",
},
map[string]interface{}{
"c": "d",
},
},
},
}
treeMap := tree.ToMap()
testMaps(t, treeMap, expected)
}
func TestTreeWriteToFloat(t *testing.T) {
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 TestOrderedEmptyTrees(t *testing.T) {
type val struct {
Key string `toml:"key"`
}
type structure struct {
First val `toml:"first"`
Empty []val `toml:"empty"`
}
input := structure{First: val{Key: "value"}}
buf := new(bytes.Buffer)
err := NewEncoder(buf).Order(OrderPreserve).Encode(input)
if err != nil {
t.Fatal("failed to encode input")
}
expected := `
[first]
key = "value"
`
if expected != buf.String() {
t.Fatal("expected and encoded body aren't equal: ", expected, buf.String())
}
}
func TestOrderedNonIncreasedLine(t *testing.T) {
type NiceMap map[string]string
type Manifest struct {
NiceMap `toml:"dependencies"`
Build struct {
BuildCommand string `toml:"build-command"`
} `toml:"build"`
}
test := &Manifest{}
test.Build.BuildCommand = "test"
buf := new(bytes.Buffer)
if err := NewEncoder(buf).Order(OrderPreserve).Encode(test); err != nil {
panic(err)
}
expected := `
[dependencies]
[build]
build-command = "test"
`
if expected != buf.String() {
t.Fatal("expected and encoded body aren't equal: ", expected, buf.String())
}
}
func TestIssue290(t *testing.T) {
tomlString :=
`[table]
"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 # `
+6
View File
@@ -0,0 +1,6 @@
package toml
// ValueStringRepresentation transforms an interface{} value into its toml string representation.
func ValueStringRepresentation(v interface{}, commented string, indent string, ord MarshalOrder, arraysOneElementPerLine bool) (string, error) {
return tomlValueStringRepresentation(v, commented, indent, ord, arraysOneElementPerLine)
}